#!/bin/sh # The SIYNET installer for macOS, Linux and Raspberry Pi, served at siy.sh: # # curl -fsSL siy.sh | sh # curl -fsSL siy.sh | sh -s -- --dry-run # # It puts siy (the command line) and siyd (the kernel) in a folder you own, without sudo, then # starts siyd at login with `siy daemon install`. Windows has its own: irm siy.sh/win | iex # # Nothing is installed until the release checks out, and every check fails closed: # 1. SHA256SUMS.minisig must be a minisign signature of SHA256SUMS by the release key below, # checked with minisign, else an OpenSSL that has Ed25519, else a pinned verifier. # 2. The archive must match its line in that signed SHA256SUMS. # # Releases live under $SIY_RELEASE_BASE (default https://siy.sh/releases): # latest the newest version, on one line # /SHA256SUMS " " for every archive # /SHA256SUMS.minisig the signature of SHA256SUMS # /siy--.tar.gz siy and siyd, at the root # tools/minisign- a verifier, for computers with nothing to check with # # Everything is in functions and the last line runs them, so a download cut short runs nothing. # Tests: apps/site/test/install.test.ts. Copy: design/boards/C-Install.dc.html. set -eu # ─── RELEASE KEY PLACEHOLDER ─── replace with the SIYNET minisign public key (the "RW…" line of # minisign.pub) before publishing. Until then this installer refuses to install anything. SIY_RELEASE_KEY='REPLACE_WITH_SIYNET_MINISIGN_PUBLIC_KEY' # ─── BOOTSTRAP VERIFIER PINS ─── the SHA-256 of $SIY_RELEASE_BASE/tools/minisign-, one # per target. They're only used where there's no minisign and no OpenSSL with Ed25519. Replace # each placeholder with the real digest before publishing. Until then that path refuses. SIY_MINISIGN_SHA256_AARCH64_APPLE_DARWIN='REPLACE_WITH_SHA256_OF_minisign-aarch64-apple-darwin' SIY_MINISIGN_SHA256_X86_64_APPLE_DARWIN='REPLACE_WITH_SHA256_OF_minisign-x86_64-apple-darwin' SIY_MINISIGN_SHA256_X86_64_UNKNOWN_LINUX_MUSL='REPLACE_WITH_SHA256_OF_minisign-x86_64-unknown-linux-musl' SIY_MINISIGN_SHA256_AARCH64_UNKNOWN_LINUX_MUSL='REPLACE_WITH_SHA256_OF_minisign-aarch64-unknown-linux-musl' SIY_MINISIGN_SHA256_ARMV7_UNKNOWN_LINUX_MUSLEABIHF='REPLACE_WITH_SHA256_OF_minisign-armv7-unknown-linux-musleabihf' # OpenSSL builds with Ed25519 that often aren't on PATH: Homebrew's, on Apple silicon and Intel. # (The openssl that comes with macOS is LibreSSL, which can't check these signatures.) SIY_OPENSSL_PATHS='/opt/homebrew/opt/openssl@3/bin/openssl /usr/local/opt/openssl@3/bin/openssl' # ── Output ────────────────────────────────────────────────────────────────────────────────────── # The boards' look: a two-space margin, one line per step, ✓ done, ! needs you, ✕ error, quiet # detail after " · ", commands in bold. Colour only in a terminal, and never with NO_COLOR. siy_style() { c_ok='' c_warn='' c_err='' c_dim='' c_link='' c_bold='' c_off='' mark='◉ siy' # Curly quotes, kept out of double-quoted strings so shellcheck stays quiet about them. lq='“' rq='”' live=0 if [ -t 1 ] && [ "${TERM:-}" != dumb ]; then live=1; fi if [ "$live" = 0 ] || [ -n "${NO_COLOR:-}" ]; then return 0; fi st_esc=$(printf '\033') # The board colours (mint, amber, coral, grey, periwinkle and the iris), exact where the # terminal says it can, else their nearest xterm-256 colours, as siy itself picks them. case ${COLORTERM:-} in truecolor | 24bit) c_ok="${st_esc}[38;2;142;240;208m" c_warn="${st_esc}[38;2;255;201;131m" c_err="${st_esc}[38;2;255;138;122m" c_dim="${st_esc}[38;2;138;143;156m" c_link="${st_esc}[4;38;2;174;188;255m" st_i0="${st_esc}[38;2;142;162;255m" st_i2="${st_esc}[38;2;255;146;194m" st_i3="${st_esc}[38;2;255;201;131m" st_i4="${st_esc}[38;2;142;240;208m" ;; *) c_ok="${st_esc}[38;5;122m" c_warn="${st_esc}[38;5;222m" c_err="${st_esc}[38;5;210m" c_dim="${st_esc}[38;5;246m" c_link="${st_esc}[4;38;5;147m" st_i0="${st_esc}[38;5;111m" st_i2="${st_esc}[38;5;211m" st_i3="${st_esc}[38;5;222m" st_i4="${st_esc}[38;5;122m" ;; esac c_bold="${st_esc}[1m" c_off="${st_esc}[0m" mark="${st_i0}◉ ${st_i2}s${st_i3}i${st_i4}y${c_off}" } siy_line() { siy_settle printf '%s\n' "$1" } siy_blank() { siy_settle printf '\n' } # At the margin, and under a mark (column 5). siy_say() { siy_line " $1"; } siy_sub() { siy_line " $1"; } # A mark, the text, and quiet detail when there is some. siy_mark() { if [ -n "${4:-}" ]; then siy_line " ${1}${2}${c_off} ${3} ${c_dim}· ${4}${c_off}" else siy_line " ${1}${2}${c_off} ${3}" fi } siy_ok() { siy_mark "$c_ok" '✓' "$1" "${2:-}"; } siy_warn() { siy_mark "$c_warn" '!' "$1" "${2:-}"; } siy_bad() { siy_mark "$c_err" '✕' "$1" "${2:-}"; } # A quiet "· …" line, with a command to run after it when there is one. siy_note() { if [ -n "${2:-}" ]; then siy_line " ${c_dim}· ${1}${c_off} ${c_bold}${2}${c_off}" else siy_line " ${c_dim}· ${1}${c_off}" fi } siy_bold() { printf '%s' "${c_bold}${1}${c_off}"; } siy_link() { printf '%s' "${c_link}${1}${c_off}"; } # A quiet line for the long download, in a terminal only. The next line printed replaces it. siy_soon() { if [ "$live" = 1 ]; then printf ' %s' "${c_dim}${1}${c_off}" pending=1 fi } siy_settle() { if [ "${pending:-0}" = 1 ]; then printf '\r\033[K' pending=0 fi } siy_header() { if [ "$header_done" = 1 ]; then return 0; fi header_done=1 hd_what='installing siyd' if [ -n "$version" ]; then hd_what="$hd_what $version"; fi hd_what="$hd_what for $platform" siy_blank if [ "$dry_run" = 1 ]; then siy_say "${mark} ${hd_what} ${c_dim}· dry run${c_off}" else siy_say "${mark} ${hd_what}" fi } # "✕ stopped" with what still holds, and the gap before the fix. siy_stopped() { siy_bad 'stopped' "nothing on this $device changed" siy_blank } # ── Where we are ──────────────────────────────────────────────────────────────────────────────── siy_detect() { os_raw=$(uname -s 2>/dev/null || true) arch_raw=$(uname -m 2>/dev/null || true) if [ -z "$os_raw" ]; then os_raw=unknown; fi if [ -z "$arch_raw" ]; then arch_raw=unknown; fi os=other os_label=$os_raw device=computer pi=0 case $os_raw in Darwin) os=macos os_label=macOS device=Mac ;; Linux) os=linux os_label=Linux if grep -q 'Raspberry Pi' /proc/device-tree/model 2>/dev/null; then pi=1 device=Pi # The 64-bit Raspberry Pi OS says ID=debian, the 32-bit one ID=raspbian. if [ ! -r /etc/os-release ] || grep -Eq '^ID="?(raspbian|debian)"?$' /etc/os-release; then os_label='Raspberry Pi OS' fi fi ;; MINGW* | MSYS* | CYGWIN* | Windows_NT) os=windows device=PC ;; esac case $arch_raw in x86_64 | amd64) arch=x86_64 arch_label=x86_64 ;; aarch64 | arm64) arch=aarch64 arch_label=arm64 ;; armv7* | armv8l) arch=armv7 arch_label=armv7 ;; *) arch=other arch_label=$arch_raw ;; esac # A Mac with Apple silicon can run this shell under Rosetta. siyd should still be native. if [ "$os" = macos ] && [ "$arch" = x86_64 ] && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" = 1 ]; then arch=aarch64 arch_label=arm64 fi case $os-$arch in macos-aarch64) target=aarch64-apple-darwin ;; macos-x86_64) target=x86_64-apple-darwin ;; linux-x86_64) target=x86_64-unknown-linux-musl ;; linux-aarch64) target=aarch64-unknown-linux-musl ;; linux-armv7) target=armv7-unknown-linux-musleabihf ;; *) target='' ;; esac platform="$os_label ($arch_label)" } siy_windows_here() { if [ "$os" != windows ]; then return 0; fi siy_blank siy_bad 'this line is for macOS and Linux' siy_blank siy_say 'On Windows, run this in PowerShell:' siy_sub "$(siy_bold 'irm siy.sh/win | iex')" siy_blank exit 1 } siy_supported() { if [ -n "$target" ]; then return 0; fi siy_blank siy_bad "siyd doesn’t run on $os_label ($arch_raw) yet" siy_blank siy_say 'It runs on macOS, Linux, Raspberry Pi and Windows.' siy_blank exit 1 } # ── Options ───────────────────────────────────────────────────────────────────────────────────── siy_options() { dry_run=0 want=latest dir_flag='' no_start=0 uninstall=0 while [ $# -gt 0 ]; do case $1 in --dry-run) dry_run=1 ;; --no-start) no_start=1 ;; --uninstall) uninstall=1 ;; --version=*) want=${1#--version=} ;; --version) if [ $# -lt 2 ]; then siy_misuse '--version needs a version, like 0.4.2'; fi want=$2 shift ;; --dir=*) dir_flag=${1#--dir=} ;; --dir) if [ $# -lt 2 ]; then siy_misuse '--dir needs a folder, like ~/.local/bin'; fi dir_flag=$2 shift ;; -h | --help) siy_help exit 0 ;; *) siy_misuse "I don’t know the option $1" ;; esac shift done if [ -z "$want" ]; then siy_misuse '--version needs a version, like 0.4.2'; fi if [ "$want" != latest ]; then want=${want#v} if ! siy_is_version "$want"; then siy_misuse "${lq}${want}${rq} isn’t a version" 'try one like 0.4.2'; fi fi } siy_misuse() { siy_blank siy_bad "$1" "${2:-}" siy_blank siy_say 'To see the options:' siy_sub "$(siy_bold 'curl -fsSL siy.sh | sh -s -- --help')" siy_blank exit 2 } siy_help() { siy_blank siy_say "${mark} This installs siy and siyd, after checking their signature." siy_blank siy_say "$(siy_bold 'curl -fsSL siy.sh | sh')" siy_say "$(siy_bold 'curl -fsSL siy.sh | sh -s -- ')" siy_blank siy_say '--dry-run check everything, change nothing' siy_say '--version install this version, not the latest' siy_say "--dir where siy and siyd go ${c_dim}· ~/.local/bin${c_off}" siy_say "--no-start don’t start siyd at login" siy_say "--uninstall take siy and siyd off this $device" siy_say '-h, --help show this' siy_blank siy_say "${c_dim}SIY_INSTALL_DIR does what --dir does. SIY_RELEASE_BASE is where releases come from.${c_off}" siy_blank } siy_is_version() { case $1 in '' | *[!0-9A-Za-z.-]*) return 1 ;; esac printf '%s\n' "$1" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$' } siy_base() { base=${SIY_RELEASE_BASE:-https://siy.sh/releases} base=${base%/} case $base in https://?* | http://?*) ;; *) siy_misuse 'SIY_RELEASE_BASE has to be a web address' 'like https://siy.sh/releases' ;; esac host=${base#*://} host=${host%%/*} host=${host##*@} case $host in \[*\]) ;; \[*\]:* | *:*) host=${host%:*} ;; esac } siy_where() { dir=${dir_flag:-${SIY_INSTALL_DIR:-}} # The default folder, a ~ in --dir=~/bin (which the shell leaves alone), and uninstalling all # need a home folder. wh_home=0 if [ -z "$dir" ] || [ "$uninstall" = 1 ]; then wh_home=1; fi case $dir in \~ | \~/*) wh_home=1 ;; esac if [ "$wh_home" = 1 ] && [ -z "${HOME:-}" ]; then siy_misuse "HOME isn’t set, so I don’t know whose folder to use" fi if [ -z "$dir" ]; then dir="$HOME/.local/bin"; fi case $dir in \~) dir=$HOME ;; \~/*) dir="$HOME/${dir#\~/}" ;; esac case $dir in /*) ;; *) dir="$(pwd)/$dir" ;; esac while [ "${dir%/}" != "$dir" ] && [ "$dir" != / ]; do dir=${dir%/}; done dir_shown=$(siy_tilde "$dir") } # A path with the home folder as ~, for showing. siy_tilde() { if [ -n "${HOME:-}" ] && [ "$HOME" != / ]; then case $1 in "$HOME") printf '~' return 0 ;; "$HOME"/*) printf '~%s' "${1#"$HOME"}" return 0 ;; esac fi printf '%s' "$1" } # The folder as it would be typed: ~/… when that's safe, otherwise the full path in quotes. siy_dir_word() { case $dir_shown in *[!A-Za-z0-9/._~+-]*) printf "'%s'" "$(printf '%s' "$dir" | sed "s/'/'\\\\''/g")" ;; *) printf '%s' "$dir_shown" ;; esac } # The line to run again, with the same options. $1 is a version ('' for the latest), $2 a folder. siy_again() { ag_flags='' if [ "$dry_run" = 1 ]; then ag_flags="$ag_flags --dry-run"; fi if [ -n "$1" ]; then ag_flags="$ag_flags --version $1"; fi if [ -n "$2" ]; then ag_flags="$ag_flags --dir $2"; fi if [ "$no_start" = 1 ]; then ag_flags="$ag_flags --no-start"; fi if [ -n "$ag_flags" ]; then printf '%s' "curl -fsSL siy.sh | sh -s --$ag_flags" else printf '%s' 'curl -fsSL siy.sh | sh' fi } siy_rerun_lines() { rl_dir='' if [ -n "$dir_flag" ]; then rl_dir=$(siy_dir_word); fi rl_version='' if [ "$want" != latest ]; then rl_version=$want; fi rerun=$(siy_again "$rl_version" "$rl_dir") rerun_latest=$(siy_again '' "$rl_dir") } # ── Downloads ─────────────────────────────────────────────────────────────────────────────────── siy_scratch() { sc_parent=${TMPDIR:-/tmp} sc_parent=${sc_parent%/} tmp=$(mktemp -d "${sc_parent:-/tmp}/siy-install.XXXXXXXX" 2>/dev/null) || tmp='' if [ -n "$tmp" ] && [ -d "$tmp" ]; then return 0; fi tmp='' siy_header siy_bad "couldn’t make a folder to download into" "in ${sc_parent:-/tmp}" siy_stopped siy_say 'Point TMPDIR at a folder you can write to, then run the same line again:' siy_sub "$(siy_bold "$rerun")" siy_blank exit 1 } siy_downloader() { if command -v curl >/dev/null 2>&1; then downloader=curl return 0 fi if command -v wget >/dev/null 2>&1; then downloader=wget wget_gnu=0 if wget --version 2>/dev/null | grep -q 'GNU Wget'; then wget_gnu=1; fi return 0 fi siy_header siy_bad 'I need curl or wget to download siyd' siy_stopped siy_say 'Install curl, then run the same line again:' siy_sub "$(siy_bold "$rerun")" siy_blank exit 1 } # siy_fetch : succeeds when the whole file arrived. Otherwise it sets fetch_kind to # missing (404), http (another error), lost (cut off part way), tls, unreachable or other, with # fetch_code, fetch_got, fetch_total and fetch_detail for the message. siy_fetch() { fe_url=$1 fe_out=$2 fetch_kind='' fetch_code='' fetch_got=0 fetch_total='' fetch_detail='' rm -f "$fe_out" "$tmp/headers" "$tmp/fetch.err" fe_rc=0 if [ "$downloader" = curl ]; then # -q ignores ~/.curlrc. Only https, even after a redirect, when the address is https. case $fe_url in https://*) set -- --proto '=https' --tlsv1.2 ;; *) set -- ;; esac fetch_code=$(curl -q -fsSL "$@" --connect-timeout 20 --speed-limit 1 --speed-time 60 \ -D "$tmp/headers" -o "$fe_out" -w '%{http_code}' "$fe_url" 2>"$tmp/fetch.err") || fe_rc=$? else set -- -T 60 if [ "$wget_gnu" = 1 ]; then set -- "$@" -t 2; fi wget "$@" -S -O "$fe_out" "$fe_url" 2>"$tmp/fetch.err" || fe_rc=$? # -S writes the server's headers among the rest, with the status lines. cp "$tmp/fetch.err" "$tmp/headers" 2>/dev/null || true fetch_code=$(sed -n 's/^ *HTTP\/[0-9.]* \([0-9][0-9][0-9]\).*/\1/p' "$tmp/headers" | tail -n 1) fi if [ "$fe_rc" = 0 ]; then # An empty body may leave no file at all; the checks after this judge what arrived. if [ ! -f "$fe_out" ]; then : >"$fe_out"; fi return 0 fi fetch_detail=$(tr -d '\r' <"$tmp/fetch.err" 2>/dev/null | grep -v '^ *$' | tail -n 1 || true) if [ -f "$fe_out" ]; then fetch_got=$(siy_size "$fe_out"); fi fetch_total=$(siy_content_length "$tmp/headers") case ${fetch_code:-000} in 000 | '') fe_heard=0 ;; *) fe_heard=1 ;; esac if [ "$downloader" = curl ]; then case $fe_rc in 22) fetch_kind=http ;; 5 | 6 | 7) fetch_kind=unreachable ;; 35 | 51 | 53 | 54 | 58 | 59 | 60 | 64 | 66 | 77 | 80 | 82 | 83 | 90 | 91) fetch_kind=tls ;; 16 | 18 | 28 | 52 | 55 | 56 | 92) fetch_kind=lost ;; *) fetch_kind=other ;; esac else case ${fetch_code:-000} in [45][0-9][0-9]) fetch_kind=http ;; 000 | '') fetch_kind=unreachable if grep -Eqi 'certificate|ssl|tls' "$tmp/fetch.err" 2>/dev/null; then fetch_kind=tls; fi ;; *) fetch_kind=lost ;; esac fi # Cut off before a byte or a status line arrived: that's not reaching it at all. if [ "$fetch_kind" = lost ] && [ "$fe_heard" = 0 ] && [ "$fetch_got" = 0 ]; then fetch_kind=unreachable fi if [ "$fetch_kind" = http ]; then case $fetch_code in 404 | 410) fetch_kind=missing ;; esac fi return 1 } # The Content-Length of the last response in a header dump (redirects come first). siy_content_length() { if [ ! -f "$1" ]; then return 0; fi tr -d '\r' <"$1" | awk ' { sub(/^[ \t]+/, "") } /^HTTP\// { cl = "" } tolower($1) == "content-length:" && $2 ~ /^[0-9]+$/ { cl = $2 } END { print cl }' } siy_size() { wc -c <"$1" | tr -d ' \t' } siy_percent() { case $fetch_total in '' | *[!0-9]*) return 0 ;; esac awk -v got="$fetch_got" -v total="$fetch_total" \ 'BEGIN { if (total > 0) { p = int(got * 100 / total); if (p > 99) p = 99; print p } }' } # Says why a download failed, and stops. $1 is what was being downloaded. siy_fetch_failed() { siy_header case $fetch_kind in lost) ff_pct=$(siy_percent) if [ -n "$ff_pct" ]; then siy_bad 'lost the network while downloading' "${ff_pct}%" else siy_bad 'lost the network while downloading' fi siy_stopped siy_say 'Check your Wi-Fi or cable, then run the same line again:' ;; unreachable) siy_bad "couldn’t reach $host" siy_stopped siy_say 'Check your Wi-Fi or cable, then run the same line again:' ;; missing) siy_bad "there’s no siyd $version for $platform yet" siy_stopped if [ "$want" != latest ]; then siy_say 'Install the latest version instead:' siy_sub "$(siy_bold "$rerun_latest")" siy_blank exit 1 fi siy_say "It isn’t out for this $device yet. Run the same line again later:" ;; tls) siy_bad "couldn’t make a secure connection to $host" siy_stopped siy_say 'A network that looks inside secure connections can do this. Try another network:' ;; http) siy_bad "$host couldn’t send $1" "HTTP $fetch_code" siy_stopped siy_say "That’s on ${host}’s side. Wait a minute, then run the same line again:" ;; *) siy_bad "couldn’t download $1" "$fetch_detail" siy_stopped siy_say 'Run the same line again:' ;; esac siy_sub "$(siy_bold "$rerun")" siy_blank exit 1 } siy_get() { if ! siy_fetch "$1" "$2"; then siy_fetch_failed "$3"; fi } siy_resolve_version() { if [ "$want" != latest ]; then version=$want return 0 fi if ! siy_fetch "$base/latest" "$tmp/latest"; then # No "latest" at all is the server's problem, not a missing build for this computer. if [ "$fetch_kind" = missing ]; then fetch_kind=http; fi siy_fetch_failed 'the latest version number' fi rv_line=$(head -n 1 "$tmp/latest" | tr -d ' \t\r') if siy_is_version "$rv_line"; then version=$rv_line return 0 fi siy_header siy_bad "$host sent a version I can’t read" siy_stopped siy_say 'A network that asks you to sign in first can do this. Sign in, or try another network:' siy_sub "$(siy_bold "$rerun")" siy_blank exit 1 } # ── Checking ──────────────────────────────────────────────────────────────────────────────────── siy_key_ready() { case $SIY_RELEASE_KEY in REPLACE_* | '') ;; *) return 0 ;; esac siy_header siy_bad "this copy of the installer doesn’t have the SIYNET release key" siy_bad 'refused to install' "nothing on this $device changed" siy_blank siy_say "Without the key I can’t check the signature, so I won’t install anything." siy_say 'Use the installer from siy.sh:' siy_sub "$(siy_bold "$rerun")" siy_blank exit 1 } # Refuses after a signature or checksum mismatch. $1 says which. siy_tampered() { siy_bad "$1" siy_bad 'refused to install' "nothing on this $device changed" siy_blank siy_say 'The download was changed somewhere on its way here.' siy_say 'Try again from another network:' siy_sub "$(siy_bold "$rerun")" siy_say "Still failing? Check the key at $(siy_link siy.sh/key)" siy_blank exit 1 } # Refuses when there's nothing trustworthy to check the signature with. siy_no_verifier() { siy_bad "${1:-there’s nothing on this $device I can check the signature with}" siy_bad 'refused to install' "nothing on this $device changed" siy_blank siy_say 'Install minisign, then run the same line again:' nv_hint=$(siy_minisign_hint) if [ -n "$nv_hint" ]; then siy_sub "$(siy_bold "$nv_hint")"; fi siy_sub "$(siy_bold "$rerun")" siy_blank exit 1 } siy_minisign_hint() { if [ "$os" = macos ]; then printf '%s' 'brew install minisign' elif command -v apt-get >/dev/null 2>&1; then printf '%s' 'sudo apt install minisign' elif command -v dnf >/dev/null 2>&1; then printf '%s' 'sudo dnf install minisign' elif command -v apk >/dev/null 2>&1; then printf '%s' 'sudo apk add minisign' elif command -v pacman >/dev/null 2>&1; then printf '%s' 'sudo pacman -S minisign' fi } siy_check_signature() { cs_sums="$tmp/SHA256SUMS" cs_sig="$tmp/SHA256SUMS.minisig" # 1. minisign itself. if command -v minisign >/dev/null 2>&1; then if minisign -V -q -P "$SIY_RELEASE_KEY" -m "$cs_sums" -x "$cs_sig" >/dev/null 2>&1; then return 0; fi siy_tampered "the signature doesn’t match the SIYNET release key" fi # 2. OpenSSL, doing what minisign does. if siy_find_openssl; then if siy_openssl_verify "$cs_sums" "$cs_sig"; then return 0; fi siy_tampered "the signature doesn’t match the SIYNET release key" fi # 3. A minisign downloaded from the release, if it's exactly the one pinned above. siy_fetch_verifier cs_rc=0 "$verifier" -V -q -P "$SIY_RELEASE_KEY" -m "$cs_sums" -x "$cs_sig" >/dev/null 2>&1 || cs_rc=$? case $cs_rc in 0) return 0 ;; 126 | 127) siy_no_verifier "I couldn’t run the signature checker I downloaded" ;; *) siy_tampered "the signature doesn’t match the SIYNET release key" ;; esac } siy_fetch_verifier() { case $target in aarch64-apple-darwin) fv_pin=$SIY_MINISIGN_SHA256_AARCH64_APPLE_DARWIN ;; x86_64-apple-darwin) fv_pin=$SIY_MINISIGN_SHA256_X86_64_APPLE_DARWIN ;; x86_64-unknown-linux-musl) fv_pin=$SIY_MINISIGN_SHA256_X86_64_UNKNOWN_LINUX_MUSL ;; aarch64-unknown-linux-musl) fv_pin=$SIY_MINISIGN_SHA256_AARCH64_UNKNOWN_LINUX_MUSL ;; armv7-unknown-linux-musleabihf) fv_pin=$SIY_MINISIGN_SHA256_ARMV7_UNKNOWN_LINUX_MUSLEABIHF ;; *) fv_pin='' ;; esac fv_pin=$(printf '%s' "$fv_pin" | tr 'A-F' 'a-f') # A placeholder pin isn't a pin: refuse rather than run something unchecked. case $fv_pin in '' | *[!0-9a-f]*) siy_no_verifier ;; esac if [ ${#fv_pin} -ne 64 ]; then siy_no_verifier; fi if ! siy_fetch "$base/tools/minisign-$target" "$tmp/minisign"; then if [ "$fetch_kind" = missing ]; then siy_no_verifier; fi siy_fetch_failed 'the signature checker' fi if ! fv_sum=$(siy_sha256 "$tmp/minisign"); then siy_no_verifier; fi if [ "$fv_sum" != "$fv_pin" ]; then siy_no_verifier "the signature checker I downloaded isn’t the one I trust" fi chmod 755 "$tmp/minisign" verifier="$tmp/minisign" } siy_sha256() { if command -v sha256sum >/dev/null 2>&1; then sha256sum <"$1" | awk '{ print tolower($1) }' elif command -v shasum >/dev/null 2>&1; then shasum -a 256 <"$1" | awk '{ print tolower($1) }' elif command -v openssl >/dev/null 2>&1; then openssl dgst -sha256 <"$1" | awk '{ print tolower($NF) }' else return 1 fi } # Finds an OpenSSL that can check an Ed25519 signature and hash with BLAKE2b, and proves it can. siy_find_openssl() { ossl='' if fo_path=$(command -v openssl 2>/dev/null) && siy_openssl_works "$fo_path"; then ossl=$fo_path return 0 fi for fo_path in $SIY_OPENSSL_PATHS; do if [ -x "$fo_path" ] && siy_openssl_works "$fo_path"; then ossl=$fo_path return 0 fi done return 1 } siy_openssl_works() { case $("$1" version 2>/dev/null || true) in 'OpenSSL 1.1.1'* | 'OpenSSL '[3-9].* | 'OpenSSL '[1-9][0-9]*) ;; *) return 1 ;; esac # RFC 8032's second test vector must check out, and the same signature on another message must # not. So must BLAKE2b-512 of "abc" (RFC 7693). Anything less and this OpenSSL isn't used. ow_dir="$tmp/probe" mkdir -p "$ow_dir" || return 1 printf '%s\n' '-----BEGIN PUBLIC KEY-----' \ 'MCowBQYDK2VwAyEAPUAXw+hDiVqStwqnTRt+vJyYLM8uxJaMwM1V8Sr0Zgw=' \ '-----END PUBLIC KEY-----' >"$ow_dir/key.pem" || return 1 printf '%s' 'kqAJqfDUyrhyDoILX2QlQKKye1QWUD+Ps3YiI+vbadoIWsHkPhWZbkWPNhPQ8R2MOHsurrQwKu6wDSkWErsMAA==' | "$1" base64 -d -A >"$ow_dir/sig" 2>/dev/null || return 1 printf 'r' >"$ow_dir/good" || return 1 printf 's' >"$ow_dir/bad" || return 1 siy_ed25519 "$1" "$ow_dir/key.pem" "$ow_dir/good" "$ow_dir/sig" || return 1 if siy_ed25519 "$1" "$ow_dir/key.pem" "$ow_dir/bad" "$ow_dir/sig"; then return 1; fi ow_hash=$(printf 'abc' | "$1" dgst -blake2b512 -binary 2>/dev/null | od -An -tx1 | tr -d ' \t\n') [ "$ow_hash" = ba80a53f981c4d0d6a2797b69f12f6e94c212f14685ac4b74b12bb6fdbffa2d17d87c5392aab792dc252d5de4533cc9518d38aa8dbf1925ab92386edd4009923 ] } # siy_ed25519 <64-byte signature> siy_ed25519() { "$1" pkeyutl -verify -pubin -inkey "$2" -rawin -in "$3" -sigfile "$4" >/dev/null 2>&1 } # The bytes of a file from an offset, in hex. siy_hex() { dd if="$1" bs=1 skip="$2" count="$3" 2>/dev/null | od -An -tx1 | tr -d ' \t\n' } # minisign's verification, step by step, with OpenSSL. See https://jedisct1.github.io/minisign/ # public key: base64 of "Ed", an 8-byte key id and the 32-byte Ed25519 key # signature file: an untrusted comment; base64 of the algorithm ("Ed" signs the file, "ED" its # BLAKE2b-512), the key id and the 64-byte signature; the trusted comment; and base64 of a # global signature over the signature followed by the trusted comment's text. siy_openssl_verify() { ov_dir="$tmp/verify" mkdir -p "$ov_dir" || return 1 printf '%s' "$SIY_RELEASE_KEY" | "$ossl" base64 -d -A >"$ov_dir/key" 2>/dev/null || return 1 [ "$(siy_size "$ov_dir/key")" = 42 ] || return 1 [ "$(dd if="$ov_dir/key" bs=1 count=2 2>/dev/null)" = Ed ] || return 1 ov_key_id=$(siy_hex "$ov_dir/key" 2 8) # An Ed25519 SubjectPublicKeyInfo is the DER prefix 302a300506032b6570032100 and the key. The # prefix is 12 bytes, so its base64 (MCowBQYDK2VwAyEA) joins the key's base64 cleanly. ov_raw=$(dd if="$ov_dir/key" bs=1 skip=10 count=32 2>/dev/null | "$ossl" base64 -A) || return 1 printf '%s\n' '-----BEGIN PUBLIC KEY-----' "MCowBQYDK2VwAyEA$ov_raw" '-----END PUBLIC KEY-----' \ >"$ov_dir/key.pem" || return 1 tr -d '\r' <"$2" >"$ov_dir/minisig" || return 1 ov_untrusted=$(sed -n 1p "$ov_dir/minisig") ov_signature=$(sed -n 2p "$ov_dir/minisig") ov_trusted=$(sed -n 3p "$ov_dir/minisig") ov_global=$(sed -n 4p "$ov_dir/minisig") case $ov_untrusted in 'untrusted comment: '*) ;; *) return 1 ;; esac case $ov_trusted in 'trusted comment: '*) ;; *) return 1 ;; esac printf '%s' "$ov_signature" | "$ossl" base64 -d -A >"$ov_dir/sig" 2>/dev/null || return 1 [ "$(siy_size "$ov_dir/sig")" = 74 ] || return 1 [ "$(siy_hex "$ov_dir/sig" 2 8)" = "$ov_key_id" ] || return 1 dd if="$ov_dir/sig" of="$ov_dir/sig.ed" bs=1 skip=10 count=64 2>/dev/null || return 1 case $(dd if="$ov_dir/sig" bs=1 count=2 2>/dev/null) in ED) "$ossl" dgst -blake2b512 -binary <"$1" >"$ov_dir/message" 2>/dev/null || return 1 ;; Ed) cp "$1" "$ov_dir/message" || return 1 ;; *) return 1 ;; esac siy_ed25519 "$ossl" "$ov_dir/key.pem" "$ov_dir/message" "$ov_dir/sig.ed" || return 1 printf '%s' "$ov_global" | "$ossl" base64 -d -A >"$ov_dir/global" 2>/dev/null || return 1 [ "$(siy_size "$ov_dir/global")" = 64 ] || return 1 cp "$ov_dir/sig.ed" "$ov_dir/global.message" || return 1 printf '%s' "${ov_trusted#trusted comment: }" >>"$ov_dir/global.message" || return 1 siy_ed25519 "$ossl" "$ov_dir/key.pem" "$ov_dir/global.message" "$ov_dir/global" } # The archive's hash in the signed SHA256SUMS (" " or " *"). siy_expected() { ex_found=$(tr -d '\r' <"$tmp/SHA256SUMS" | awk -v name="$archive" ' NF == 2 { file = $2; sub(/^\*/, "", file); if (file == name) print tolower($1) }' | sort -u) if [ -z "$ex_found" ]; then # Signed, and this computer's build isn't in it. fetch_kind=missing siy_fetch_failed "siyd $version" fi case $ex_found in *[!0-9a-f]*) siy_tampered "the download doesn’t match its signed checksum" ;; esac if [ ${#ex_found} -ne 64 ]; then siy_tampered "the download doesn’t match its signed checksum"; fi expected=$ex_found } siy_check_sum() { if ! cs_actual=$(siy_sha256 "$tmp/$archive"); then siy_bad "there’s nothing here I can check the download with" 'no sha256sum or shasum' siy_bad 'refused to install' "nothing on this $device changed" siy_blank siy_say 'Install sha256sum (it comes with coreutils), then run the same line again:' siy_sub "$(siy_bold "$rerun")" siy_blank exit 1 fi if [ "$cs_actual" != "$expected" ]; then siy_tampered "the download doesn’t match its signed checksum" fi } siy_unpack() { mkdir "$tmp/x" if tar -xzf "$tmp/$archive" -C "$tmp/x" >/dev/null 2>&1 && [ -f "$tmp/x/siy" ] && [ -f "$tmp/x/siyd" ]; then return 0 fi siy_bad "the download doesn’t have siy and siyd in it" siy_bad 'refused to install' "nothing on this $device changed" siy_blank siy_say "That’s a problem with this release, not with this $device. Run the same line again later:" siy_sub "$(siy_bold "$rerun")" siy_blank exit 1 } # ── Installing ────────────────────────────────────────────────────────────────────────────────── siy_on_path() { case ":${PATH:-}:" in *":$dir:"* | *":$dir/:"*) return 0 ;; esac return 1 } siy_put() { if ! mkdir -p "$dir" 2>/dev/null; then siy_cant_write; fi pf_siy="$dir/.siy.new.$$" pf_siyd="$dir/.siyd.new.$$" if ! { cp "$tmp/x/siy" "$pf_siy" && cp "$tmp/x/siyd" "$pf_siyd" && chmod 755 "$pf_siy" "$pf_siyd"; } 2>/dev/null; then rm -f "$pf_siy" "$pf_siyd" siy_cant_write fi # Renaming over the old files is atomic: a running siyd keeps the copy it started from. changed=1 if ! mv -f "$pf_siyd" "$dir/siyd" 2>/dev/null || ! mv -f "$pf_siy" "$dir/siy" 2>/dev/null; then rm -f "$pf_siy" "$pf_siyd" siy_cant_write fi } siy_cant_write() { siy_bad "couldn’t put siy and siyd in $dir_shown" if [ "$changed" = 1 ]; then siy_bad 'stopped part way' 'run the same line again to finish' siy_blank else siy_stopped fi if [ "$dir" = "${HOME:-}/.local/bin" ]; then siy_say "Check you can write to $dir_shown, then run the same line again:" siy_sub "$(siy_bold "$rerun")" else siy_say 'Choose a folder you can write to:' cw_version='' if [ "$want" != latest ]; then cw_version=$want; fi cw_dir='' if [ -n "${HOME:-}" ]; then cw_dir=$(siy_tilde "$HOME/.local/bin"); fi siy_sub "$(siy_bold "$(siy_again "$cw_version" "$cw_dir")")" fi siy_blank exit 1 } siy_start() { daemon_ok=0 if [ "$no_start" = 1 ]; then siy_note 'start siyd at login with' 'siy daemon install' return 0 fi # launchd on a Mac, a systemd user unit on Linux; neither needs sudo. if "$dir/siy" daemon install "$tmp/daemon.log" 2>&1; then daemon_ok=1 # siyd names this device after its host name, without .local. sd_name=$(uname -n 2>/dev/null || true) sd_name=${sd_name%.local} if [ -n "$sd_name" ]; then siy_ok 'siyd is running' "this $device is ${lq}${sd_name}${rq}" else siy_ok 'siyd is running' fi fi } siy_pi() { if [ "$pi" = 0 ]; then return 0; fi pi_model=$(tr -d '\000' 2>/dev/null /dev/null || true) pi_mem=$(awk -v kb="${pi_kb:-0}" 'BEGIN { gb = kb / 1048576 if (gb >= 0.75) printf "%d GB", gb + 0.5; else if (kb > 0) printf "%d MB", kb / 1024 + 0.5 }') siy_ok "found a ${pi_model:-Raspberry Pi}" "$pi_mem" } # Warnings go last, each above the line that fixes it. siy_warnings() { if [ "$no_start" = 0 ] && [ "$daemon_ok" = 0 ]; then siy_warn "I couldn’t start siyd at login" "it’s installed, and runs by hand with siyd" siy_sub "$(siy_bold 'siy daemon install')" fi if siy_on_path; then return 0; fi case $dir in *\'* | *\"* | *\$* | *\`* | *\\*) siy_warn "$dir_shown isn’t on your PATH" 'add it, then open a new terminal' return 0 ;; esac wn_expr=$dir if [ -n "${HOME:-}" ] && [ "$HOME" != / ]; then case $dir in "$HOME"/*) wn_expr="\$HOME${dir#"$HOME"}" ;; esac fi wn_shell=${SHELL:-} case ${wn_shell##*/} in fish) wn_fix="fish_add_path $(siy_dir_word)" ;; zsh) wn_fix="echo 'export PATH=\"$wn_expr:\$PATH\"' >> ~/.zprofile" ;; bash) wn_fix="echo 'export PATH=\"$wn_expr:\$PATH\"' >> ~/.bashrc" ;; *) wn_fix="echo 'export PATH=\"$wn_expr:\$PATH\"' >> ~/.profile" ;; esac siy_warn "$dir_shown isn’t on your PATH" 'add it, then open a new terminal' siy_sub "$(siy_bold "$wn_fix")" } # What to do next. A device already in the SIYNET makes the code; this one types it. siy_next() { siy_blank siy_say "$(siy_bold 'Join a SIYNET')" siy_sub 'on a device you already have, open Add a device' siy_sub "then type its code here $(siy_bold 'siy join ')" siy_blank siy_say "Or start a new one: this $device is ready. $(siy_bold 'siy status')" siy_blank } siy_dry_report() { siy_note "would put siy and siyd in $dir_shown" if [ "$no_start" = 0 ]; then siy_note 'would start siyd at login with' 'siy daemon install'; fi if ! siy_on_path; then siy_note "would ask you to add $dir_shown to your PATH"; fi siy_blank siy_say "Nothing on this $device changed." siy_blank } siy_install() { siy_supported if [ "$want" != latest ]; then version=$want; fi siy_key_ready siy_scratch siy_downloader siy_resolve_version siy_header archive="siy-$version-$target.tar.gz" siy_get "$base/$version/SHA256SUMS" "$tmp/SHA256SUMS" 'the signed checksums' siy_get "$base/$version/SHA256SUMS.minisig" "$tmp/SHA256SUMS.minisig" 'the signature' siy_check_signature siy_expected siy_soon "… downloading siyd $version" siy_get "$base/$version/$archive" "$tmp/$archive" "siyd $version" siy_check_sum siy_ok 'checked the signature' 'SIYNET release key' siy_unpack if [ "$dry_run" = 1 ]; then siy_pi siy_dry_report return 0 fi siy_put siy_ok 'installed siy and siyd' "$dir_shown" siy_start siy_pi siy_warnings siy_next } # ── Uninstalling ──────────────────────────────────────────────────────────────────────────────── siy_uninstall() { un_siy="$dir/siy" un_siyd="$dir/siyd" # A siy that knows `uninstall` does it properly: it asks first, and takes this device out of # the SIYNET. It may ask on the terminal, so give it the terminal rather than this script. if [ -f "$un_siy" ] && [ -x "$un_siy" ] && "$un_siy" help uninstall /dev/null 2>&1; then if [ "$dry_run" = 1 ]; then set -- --dry-run; else set --; fi if (exec /dev/null; then exec "$un_siy" uninstall "$@" /dev/null || true) case $un_other in /*/siy) siy_warn "there’s a siy in $(siy_tilde "${un_other%/siy}")" 'to remove that one' dir=${un_other%/siy} dir_shown=$(siy_tilde "$dir") siy_sub "$(siy_bold "curl -fsSL siy.sh | sh -s -- --uninstall --dir $(siy_dir_word)")" ;; esac siy_blank return 0 fi siy_say 'It will:' if [ "$un_service_was" = 1 ]; then siy_sub 'stop siyd, and stop it starting at login'; fi siy_sub "remove siy and siyd from $dir_shown" if [ -d "$un_data" ]; then siy_say "${c_dim}Your data stays in $(siy_tilde "$un_data").${c_off}" siy_say "${c_dim}This $device stays in your SIYNET until you remove it in Devices on another device.${c_off}" fi siy_blank if [ "$dry_run" = 1 ]; then siy_say "Nothing on this $device changed." siy_blank return 0 fi if [ "$un_service_was" = 1 ]; then if [ -x "$un_siy" ]; then "$un_siy" daemon uninstall /dev/null 2>&1 || true; fi # siy wasn't there, or couldn't: unload the login item the way siy daemon would. if [ -e "$un_service" ]; then case $os in macos) launchctl bootout "gui/$(id -u)/ai.siynet.siyd" >/dev/null 2>&1 || true ;; *) systemctl --user disable --now siyd.service >/dev/null 2>&1 || true ;; esac rm -f "$un_service" 2>/dev/null || true if [ "$os" != macos ]; then systemctl --user daemon-reload >/dev/null 2>&1 || true; fi fi if [ -e "$un_service" ]; then siy_warn "I couldn’t stop siyd starting at login" "remove $(siy_tilde "$un_service")" else siy_ok 'stopped siyd' fi fi rm -f "$un_siy" "$un_siyd" 2>/dev/null || true if [ -e "$un_siy" ] || [ -e "$un_siyd" ]; then siy_bad "couldn’t remove siy and siyd from $dir_shown" siy_blank siy_say 'Check you can write to that folder, then run the same line again.' siy_blank exit 1 fi siy_ok 'removed siy and siyd' "$dir_shown" siy_blank siy_say "SIY is gone from this $device. To bring it back:" siy_sub "$(siy_bold 'curl -fsSL siy.sh | sh')" siy_blank } # ── Running ───────────────────────────────────────────────────────────────────────────────────── # These two only run from traps, which older shellcheck releases don't follow. # shellcheck disable=SC2317 siy_cleanup() { if [ -n "${pf_siy:-}" ]; then rm -f "$pf_siy" "$pf_siyd" 2>/dev/null; fi if [ -n "${tmp:-}" ] && [ -d "$tmp" ]; then rm -rf "$tmp"; fi return 0 } # shellcheck disable=SC2317 siy_interrupted() { trap - INT TERM HUP siy_settle if [ "${changed:-0}" = 0 ] && [ -n "${device:-}" ]; then siy_blank siy_stopped fi exit "$1" } siy_main() { # Byte-wise text tools, whatever the terminal's language. LC_ALL=C export LC_ALL tmp='' pending=0 changed=0 header_done=0 version='' pf_siy='' pf_siyd='' daemon_ok=0 siy_status=0 # Clean up on every exit, keeping the exit status. trap 'siy_status=$?; set +e; siy_cleanup; exit "$siy_status"' EXIT trap 'siy_interrupted 130' INT trap 'siy_interrupted 143' TERM trap 'siy_interrupted 129' HUP siy_style siy_detect siy_options "$@" siy_windows_here siy_base siy_where siy_rerun_lines if [ "$uninstall" = 1 ]; then siy_uninstall else siy_install fi } siy_main "$@"