-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·425 lines (372 loc) · 16.1 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·425 lines (372 loc) · 16.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
#!/bin/sh
# SPDX-License-Identifier: MIT OR Apache-2.0
#
# mkit installer — downloads the signed release archive matching the
# host OS + architecture from a public GitHub Release, verifies its
# cosign signature, verifies its SHA256, and installs the `mkit` binary
# atomically into $MKIT_INSTALL_DIR (default: ~/.local/bin).
#
# This script targets Darwin and Linux (including WSL, which runs the real
# Linux binary). Native Windows (PowerShell, no POSIX layer) has its own
# installer: install.ps1 — `irm https://mkit.sh/install.ps1 | iex`.
#
# Usage:
# curl mkit.sh | sh
# curl -sSfL https://mkit.sh/install.sh | sh
# curl -sSfL https://mkit.sh/install.sh | sh -s -- --version v0.3.0
# # The raw GitHub URL serves the same script as a fallback:
# # curl -sSfL https://raw.githubusercontent.com/officialunofficial/mkit/main/install.sh | sh
#
# Trust model:
# * cosign keyless verification is REQUIRED by default. The script
# fetches the per-archive `.cosign.bundle`, validates that it was
# signed by this repo's release.yml workflow at a strict-semver tag,
# and only then installs. The published .sha256 file is fetched
# from the same origin as the archive and proves NOTHING about
# authenticity on its own — it's a defense-in-depth integrity check.
# * If `cosign` is not in PATH on a release-track install (i.e. you
# have not passed --insecure-skip-cosign), the script exits non-zero
# with installation instructions. Use --insecure-skip-cosign to
# proceed without signature verification at your own risk.
#
# Version pinning:
# * MKIT_VERSION (or --version) pins the install to a specific tag.
# RECOMMENDED for production use — it's the only way to get a
# repeatable install.
# * Without a version pin the script resolves the GitHub "latest"
# release. The resolved tag is recorded at
# ~/.local/state/mkit/installed-tag; subsequent unpinned installs
# compare against this file and warn loudly on a silent downgrade.
#
# Environment overrides:
# MKIT_VERSION explicit tag (e.g. v0.3.0). Default: 'latest'.
# MKIT_INSTALL_DIR install prefix. Default: ~/.local/bin.
# MKIT_STATE_DIR state dir for installed-tag bookkeeping.
# Default: ~/.local/state/mkit.
# MKIT_SKIP_COSIGN set to '1' to skip cosign verification
# (equivalent to --insecure-skip-cosign).
#
# Flags:
# --version <tag> pin to a specific release tag.
# --prefix <dir> install prefix.
# --insecure-skip-cosign skip cosign signature verification (DANGEROUS).
# --cosign deprecated; cosign is on by default. No-op.
# --help, -h show this help text.
#
# For private-repo installs, use `gh release download` instead — it
# handles auth natively:
# gh release download v0.3.0 --repo officialunofficial/mkit \
# --pattern 'mkit-*-<target>.tar.gz' --dir .
#
# POSIX sh — runs under dash/ash/bash/zsh without bashisms.
set -eu
owner="officialunofficial"
repo="mkit"
api="https://api.github.com/repos/${owner}/${repo}"
dl="https://github.com/${owner}/${repo}/releases/download"
log() { printf '==> %s\n' "$*"; }
warn() { printf 'warning: %s\n' "$*" >&2; }
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
# Print the embedded usage block. Reading from $0 breaks under
# `curl … | sh` because $0 there is `sh`. Instead we embed the help
# text here.
print_help() {
cat <<'EOF'
mkit installer
Usage:
curl mkit.sh | sh
curl -sSfL https://mkit.sh/install.sh | sh
sh install.sh [--version <tag>] [--prefix <dir>] [--insecure-skip-cosign]
Flags:
--version <tag> Pin to a specific release tag (e.g. v0.3.0).
RECOMMENDED for production / reproducible installs.
--prefix <dir> Install prefix. Default: ~/.local/bin.
--insecure-skip-cosign Skip cosign signature verification. DANGEROUS — use
only when cosign is unavailable and you have
verified the binary by other means.
--cosign Deprecated alias. cosign verification is now ON by
default; this flag is a no-op kept for back-compat.
--help, -h Show this help.
Environment:
MKIT_VERSION Same as --version.
MKIT_INSTALL_DIR Same as --prefix.
MKIT_STATE_DIR State dir for downgrade bookkeeping.
Default: ~/.local/state/mkit.
MKIT_SKIP_COSIGN=1 Same as --insecure-skip-cosign.
Trust model:
cosign keyless signature verification is REQUIRED by default. Install cosign
from https://docs.sigstore.dev/cosign/installation/ before running, or pass
--insecure-skip-cosign at your own risk.
EOF
}
version="${MKIT_VERSION:-}"
install_dir="${MKIT_INSTALL_DIR:-$HOME/.local/bin}"
state_dir="${MKIT_STATE_DIR:-$HOME/.local/state/mkit}"
skip_cosign="${MKIT_SKIP_COSIGN:-}"
while [ $# -gt 0 ]; do
case "$1" in
--version) [ $# -ge 2 ] || die "--version requires a tag (e.g. --version v0.3.0)"; version="$2"; shift 2 ;;
--prefix) [ $# -ge 2 ] || die "--prefix requires a directory (e.g. --prefix /usr/local/bin)"; install_dir="$2"; shift 2 ;;
--insecure-skip-cosign) skip_cosign="1"; shift ;;
--cosign) shift ;; # deprecated; cosign is default-on
--help|-h) print_help; exit 0 ;;
*) die "unknown argument: $1 (try --help)" ;;
esac
done
fetch() {
url="$1"; out="$2"
if command -v curl >/dev/null 2>&1; then
curl -sSfL "$url" -o "$out"
elif command -v wget >/dev/null 2>&1; then
wget -qO "$out" "$url"
else
die "need curl or wget to fetch $url"
fi
}
fetch_stdout() {
url="$1"
if command -v curl >/dev/null 2>&1; then
curl -sSfL "$url"
elif command -v wget >/dev/null 2>&1; then
wget -qO- "$url"
else
die "need curl or wget"
fi
}
# ---- host detection ----
uname_os="$(uname -s 2>/dev/null || echo unknown)"
uname_arch="$(uname -m 2>/dev/null || echo unknown)"
case "$uname_os" in
Darwin) os_part="apple-darwin" ;;
Linux) os_part="unknown-linux-gnu" ;; # Also matches WSL, which runs the real Linux binary.
MINGW*|MSYS*|CYGWIN*)
die "this is install.sh (POSIX sh) running under a Windows POSIX layer ($uname_os) — use the native PowerShell installer instead: irm https://mkit.sh/install.ps1 | iex"
;;
*) die "unsupported OS: $uname_os (only Darwin and Linux have prebuilt binaries; Windows users see install.ps1)" ;;
esac
case "$uname_arch" in
x86_64|amd64) arch_part="x86_64" ;;
arm64|aarch64) arch_part="aarch64" ;;
*) die "unsupported arch: $uname_arch" ;;
esac
target="${arch_part}-${os_part}"
# ---- version resolution ----
resolved_from_latest=0
if [ -z "$version" ]; then
log "resolving latest release tag (no --version pinned)"
warn "for reproducible installs, pin a tag with --version vX.Y.Z or MKIT_VERSION=vX.Y.Z"
version=$(fetch_stdout "${api}/releases/latest" \
| sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' \
| head -n1)
[ -n "$version" ] || die "could not parse latest release tag"
resolved_from_latest=1
fi
# Normalise (accept either 'v0.1.0' or '0.1.0').
case "$version" in
v*) tag="$version"; bare="${version#v}" ;;
*) tag="v$version"; bare="$version" ;;
esac
# ---- silent-downgrade guard ----
#
# We compare the previously-installed tag against the tag we're about
# to install. POSIX sh has no built-in semver compare, so we use sort
# -V (GNU/BSD coreutils both ship it). If sort -V is missing we just
# warn and continue.
#
# Two records are consulted:
# 1. $state_dir/installed-tag — global per-user record, survives
# install_dir rotation.
# 2. $install_dir/.mkit-installed-tag — local-to-binary record,
# survives state-dir clearance and catches the case where a user
# runs the installer with a different MKIT_STATE_DIR.
# Both are written on every install; on a downgrade attempt, EITHER
# file disagreeing causes the guard to fire.
state_file="${state_dir}/installed-tag"
bin_state_file="${install_dir}/.mkit-installed-tag"
read_state_file() {
if [ -f "$1" ]; then
cat "$1" 2>/dev/null || true
fi
}
prev_tag_global="$(read_state_file "$state_file")"
prev_tag_local="$(read_state_file "$bin_state_file")"
# Pick the higher of the two so a missing/empty file never lets a
# downgrade slip through. sort -V handles the comparison; if either is
# empty we just use the non-empty one.
if [ -n "$prev_tag_global" ] && [ -n "$prev_tag_local" ]; then
prev_g_bare="${prev_tag_global#v}"
prev_l_bare="${prev_tag_local#v}"
if command -v sort >/dev/null 2>&1; then
prev_higher="$(printf '%s\n%s\n' "$prev_g_bare" "$prev_l_bare" | sort -V | tail -n1)"
if [ "$prev_higher" = "$prev_l_bare" ]; then
prev_tag="$prev_tag_local"
else
prev_tag="$prev_tag_global"
fi
else
prev_tag="$prev_tag_global"
fi
elif [ -n "$prev_tag_global" ]; then
prev_tag="$prev_tag_global"
else
prev_tag="$prev_tag_local"
fi
if [ -n "$prev_tag" ] && [ "$prev_tag" != "$tag" ]; then
if command -v sort >/dev/null 2>&1; then
# Strip leading 'v' for sort -V.
prev_bare="${prev_tag#v}"
new_bare="${bare}"
higher="$(printf '%s\n%s\n' "$prev_bare" "$new_bare" | sort -V | tail -n1)"
if [ "$higher" = "$prev_bare" ] && [ "$prev_bare" != "$new_bare" ]; then
warn "you are installing $tag but $prev_tag is already recorded — this is a DOWNGRADE."
if [ "$resolved_from_latest" = "1" ]; then
die "refusing to silently downgrade from $prev_tag to $tag via 'latest'. Pin --version $prev_tag or newer, or delete $state_file and $bin_state_file."
else
warn "proceeding because --version was passed explicitly."
fi
fi
fi
fi
# Independent check: the two state files MUST agree when both exist.
# A mismatch means one was tampered with (or a parallel installer
# misbehaved). Refuse rather than picking a winner silently.
if [ -n "$prev_tag_global" ] && [ -n "$prev_tag_local" ] \
&& [ "$prev_tag_global" != "$prev_tag_local" ]; then
die "installed-tag mismatch: $state_file says '$prev_tag_global' but $bin_state_file says '$prev_tag_local'. Refusing to install. Resolve manually."
fi
archive="mkit-${bare}-${target}.tar.gz"
archive_url="${dl}/${tag}/${archive}"
sha_url="${archive_url}.sha256"
bundle_url="${archive_url}.cosign.bundle"
log "installing mkit $tag ($target) into $install_dir"
tmp=$(mktemp -d -t mkit-install.XXXXXXXXXX)
trap 'rm -rf "$tmp"' EXIT INT TERM
# ---- download ----
log "fetching $archive_url"
fetch "$archive_url" "$tmp/$archive" \
|| die "download failed — the release may not include a build for $target"
log "fetching ${archive}.sha256"
fetch "$sha_url" "$tmp/${archive}.sha256"
# ---- cosign verification (default-on) ----
#
# This is the AUTHENTICATION step. The .sha256 check below is integrity
# only — it shows nothing about provenance because the file is served
# from the same origin as the archive. Cosign verifies that this archive
# was produced by this repo's release.yml workflow at a strict-semver tag.
if [ "$skip_cosign" = "1" ]; then
warn "cosign verification SKIPPED (--insecure-skip-cosign / MKIT_SKIP_COSIGN=1)"
warn "the only authenticity check disabled. SHA256 alone proves nothing."
else
if ! command -v cosign >/dev/null 2>&1; then
cat >&2 <<EOF
error: cosign is not in PATH.
cosign keyless signature verification is required by default — without it
the .sha256 file you just downloaded proves nothing about authenticity
(it comes from the same origin as the archive). Install cosign from:
https://docs.sigstore.dev/cosign/installation/
Or, if you have verified the binary by other means and accept the risk,
re-run with --insecure-skip-cosign (or MKIT_SKIP_COSIGN=1).
EOF
exit 1
fi
log "fetching cosign bundle"
fetch "$bundle_url" "$tmp/${archive}.cosign.bundle" \
|| die "cosign bundle missing for $tag — refusing to install. Pass --insecure-skip-cosign to override."
log "cosign verify-blob"
# Strict-semver regex on the tag, anchored to the exact workflow path.
# Accepts e.g. v0.1.0 and v1.2.3-rc.1 but rejects v.* glob matches,
# branch refs, and rogue workflows in the same repo.
cosign verify-blob \
--certificate-identity-regexp '^https://github\.com/officialunofficial/mkit/\.github/workflows/release\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$' \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--bundle "$tmp/${archive}.cosign.bundle" \
"$tmp/$archive" \
|| die "cosign verification failed — refusing to install"
fi
# ---- integrity (SHA256) ----
#
# Defense-in-depth integrity check. Comes AFTER cosign because cosign
# is the authenticity check; sha256 only catches transport corruption.
log "verifying SHA256"
cd "$tmp"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum -c "${archive}.sha256" >/dev/null
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 -c "${archive}.sha256" >/dev/null
else
die "need sha256sum or shasum to verify the download"
fi
# ---- extract + install (atomic) ----
log "extracting"
tar -xzf "$archive"
stage_dir="mkit-${bare}-${target}"
bin_src="$tmp/$stage_dir/mkit"
[ -x "$bin_src" ] || die "extracted archive missing expected binary at $stage_dir/mkit"
mkdir -p "$install_dir"
install_path="$install_dir/mkit"
# ---- install_dir permission check ----
#
# A group- or world-writable install directory lets anyone on the host
# race a malicious binary into $install_path between our final mv(1)
# and the user's first invocation. Refuse rather than win the race
# unintentionally. Mode is read with BSD stat on Darwin and GNU stat
# on Linux — the rest of the script already keys on $uname_os.
case "$uname_os" in
Darwin) dir_mode="$(stat -f '%Lp' "$install_dir" 2>/dev/null || echo '')" ;;
Linux) dir_mode="$(stat -c '%a' "$install_dir" 2>/dev/null || echo '')" ;;
*) dir_mode="" ;;
esac
if [ -n "$dir_mode" ]; then
# Normalise to three digits so the substring tests below align.
case "$dir_mode" in
[0-9]) dir_mode="00${dir_mode}" ;;
[0-9][0-9]) dir_mode="0${dir_mode}" ;;
esac
# Group bit = middle digit, other bit = last digit. Either even-2
# (write bit set) is the failure case.
group_digit="$(printf '%s' "$dir_mode" | cut -c2)"
other_digit="$(printf '%s' "$dir_mode" | cut -c3)"
case "$group_digit" in
[2367]) die "install dir $install_dir is group-writable (mode $dir_mode); refusing to install — a malicious local user could race a replacement binary into place between mv and first execution. Tighten permissions: chmod g-w $install_dir" ;;
esac
case "$other_digit" in
[2367]) die "install dir $install_dir is world-writable (mode $dir_mode); refusing to install — anyone on this host could race a replacement binary into place. Tighten permissions: chmod o-w $install_dir" ;;
esac
fi
# Atomic install: stage the new binary in the SAME directory as the
# final install path (so rename(2) stays within one filesystem),
# chmod +x BEFORE the rename so the final inode is never executable-
# without-content, then rename on top of the old binary. mv on POSIX
# uses rename(2) when src and dst are on the same fs, which is atomic.
staged="$(mktemp "${install_path}.XXXXXX")" \
|| die "could not create temp file in ${install_dir}"
# mktemp creates with restrictive perms; copy contents then mark exec.
cp "$bin_src" "$staged"
chmod 0755 "$staged"
mv "$staged" "$install_path"
# ---- record installed tag for downgrade guard ----
#
# Write BOTH copies. Future installer runs treat either as
# authoritative (the higher of the two wins); a mismatch trips the
# tamper check above. mv-from-staging makes the writes atomic.
mkdir -p "$state_dir"
printf '%s\n' "$tag" > "${state_file}.new"
mv "${state_file}.new" "$state_file"
printf '%s\n' "$tag" > "${bin_state_file}.new"
mv "${bin_state_file}.new" "$bin_state_file"
log "installed $install_path ($tag)"
# ---- PATH check ----
case ":$PATH:" in
*":$install_dir:"*)
"$install_path" version
;;
*)
warn "$install_dir is not in PATH"
cat <<EOF
Add it to your shell profile:
export PATH="$install_dir:\$PATH"
Then re-open the shell, or run directly:
$install_path version
EOF
;;
esac