feat: the one-line installer verifies the CI-signed release manifest and pulls the image by digest [CI]
This commit is contained in:
1 parent
a19e6548d3
commit
310b2b5b19
8 files changed
+227
-8
No files matched your search
@@ -1,13 +1,41 @@
|
||||
# MoShiro 节点一键安装(Windows,Agent 和 Probe 共用同一份)。面板在 /install.ps1?code=… 生成这份脚本,把下面四个值填好:
|
||||
# irm 'https://panel/install.ps1?code=jt_…' | iex
|
||||
# 镜像:向面板取 CI 签名的发布清单,用内置的发布公钥验签,再按清单里的 digest 拉取。面板还没有签名的清单时,设 $env:MOSHIRO_ALLOW_UNVERIFIED='1' 才会按标签安装。
|
||||
# 做的事:用 Docker Desktop 起一个容器,身份和配置存在命名卷里(重启、换新镜像重建都不用再给参数),节点名字默认取计算机名。
|
||||
# 可选环境变量:MOSHIRO_NAME=节点名字(名字被占用时用)。
|
||||
function Test-MoShiroSignature([byte[]]$data, [byte[]]$derSignature, [string]$pem) {
|
||||
# 发布公钥是 PEM 的 SPKI(P-256):最后 64 字节是 X、Y;签名是 DER,.NET 要 r||s
|
||||
$b64 = ($pem -split "`n" | Where-Object { $_ -notmatch '^-----' }) -join ''
|
||||
$spki = [Convert]::FromBase64String($b64.Trim())
|
||||
$xy = $spki[($spki.Length - 64)..($spki.Length - 1)]
|
||||
$blob = [byte[]]@(0x45, 0x43, 0x53, 0x31, 0x20, 0, 0, 0) + $xy
|
||||
$i = 2
|
||||
if ($derSignature[1] -band 0x80) { $i = 2 + ($derSignature[1] -band 0x7f) }
|
||||
$parts = @()
|
||||
for ($n = 0; $n -lt 2; $n++) {
|
||||
$len = $derSignature[$i + 1]
|
||||
$part = [byte[]]$derSignature[($i + 2)..($i + 1 + $len)]
|
||||
while ($part.Length -gt 32 -and $part[0] -eq 0) { $part = [byte[]]$part[1..($part.Length - 1)] }
|
||||
if ($part.Length -gt 32) { return $false }
|
||||
$parts += , ([byte[]](New-Object byte[] (32 - $part.Length)) + $part)
|
||||
$i += 2 + $len
|
||||
}
|
||||
$key = [Security.Cryptography.CngKey]::Import($blob, [Security.Cryptography.CngKeyBlobFormat]::EccPublicBlob)
|
||||
try {
|
||||
$ecdsa = New-Object Security.Cryptography.ECDsaCng($key)
|
||||
return $ecdsa.VerifyData($data, [byte[]]($parts[0] + $parts[1]), [Security.Cryptography.HashAlgorithmName]::SHA256)
|
||||
} finally { $key.Dispose() }
|
||||
}
|
||||
|
||||
function Install-MoShiro {
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$role = '@@ROLE@@'
|
||||
$server = '@@SERVER@@'
|
||||
$code = '@@CODE@@'
|
||||
$image = '@@IMAGE@@'
|
||||
$releaseKey = @'
|
||||
@@RELEASE_KEY@@
|
||||
'@
|
||||
|
||||
$name = if ($env:MOSHIRO_NAME) { $env:MOSHIRO_NAME } else { $env:COMPUTERNAME }
|
||||
$name = $name -replace '[^A-Za-z0-9._-]', '-'
|
||||
@@ -34,6 +62,34 @@ function Install-MoShiro {
|
||||
return
|
||||
}
|
||||
|
||||
# 验发布清单:签名对不上、组件不对、digest 格式不对都直接拒绝,不装
|
||||
$client = New-Object System.Net.WebClient
|
||||
$manifest = $null
|
||||
try {
|
||||
$manifest = $client.DownloadData("$server/install/manifest?code=$code")
|
||||
$signature = [Convert]::FromBase64String(($client.DownloadString("$server/install/manifest.sig?code=$code")).Trim())
|
||||
} catch {
|
||||
$manifest = $null
|
||||
}
|
||||
if ($null -eq $manifest) {
|
||||
if ($env:MOSHIRO_ALLOW_UNVERIFIED -ne '1') {
|
||||
Write-Error "the panel has no signed release for $role yet, so the image cannot be verified. Run again with `$env:MOSHIRO_ALLOW_UNVERIFIED='1' to install $image without verification"
|
||||
return
|
||||
}
|
||||
Write-Host "warning: installing $image without signature verification"
|
||||
} else {
|
||||
if (-not (Test-MoShiroSignature $manifest $signature $releaseKey)) {
|
||||
Write-Error 'the release signature does not verify against the built-in release key; refusing to install'
|
||||
return
|
||||
}
|
||||
$release = [Text.Encoding]::UTF8.GetString($manifest) | ConvertFrom-Json
|
||||
if ($release.component -ne $role) { Write-Error "the signed release is for '$($release.component)', not '$role'"; return }
|
||||
if ($release.digest -notmatch '^sha256:[0-9a-f]{64}$') { Write-Error 'the signed release has a malformed digest'; return }
|
||||
if ($release.image -notmatch '^[A-Za-z0-9._/:-]+$') { Write-Error 'the signed release has a malformed image name'; return }
|
||||
$image = "$($release.image)@$($release.digest)"
|
||||
Write-Host "verified release $($release.version) ($($release.tag)): $image"
|
||||
}
|
||||
|
||||
Write-Host "pulling $image"
|
||||
docker pull $image | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
# MoShiro 节点一键安装(Agent 和 Probe 共用同一份)。面板在 /install?code=… 生成这份脚本,把下面四个值填好:
|
||||
# curl -fsSL 'https://panel/install?code=jt_…' | bash (macOS 用 zsh)
|
||||
# 做的事:用 Docker 或 Podman 起一个容器,身份和配置存在命名卷里(重启、换新镜像重建都不用再给参数),节点名字默认取主机名。
|
||||
# 可选环境变量:MOSHIRO_NAME=节点名字(名字被占用时用)、MOSHIRO_ENGINE=docker|podman、MOSHIRO_DRY_RUN=1(只打印命令)。
|
||||
# 可选环境变量:MOSHIRO_NAME=节点名字(名字被占用时用)、MOSHIRO_ENGINE=docker|podman、MOSHIRO_DRY_RUN=1(只打印命令)、
|
||||
# MOSHIRO_ALLOW_UNVERIFIED=1(面板还没有已签名的发布清单时,不验签、按标签安装;默认拒绝)。
|
||||
# 镜像:向面板取 CI 签名的发布清单,用下面内置的发布公钥验签,再按清单里的 digest 拉取(镜像按内容寻址,换不了)。
|
||||
set -eu
|
||||
|
||||
ROLE='@@ROLE@@'
|
||||
SERVER='@@SERVER@@'
|
||||
CODE='@@CODE@@'
|
||||
IMAGE='@@IMAGE@@'
|
||||
RELEASE_KEY='@@RELEASE_KEY@@'
|
||||
|
||||
say() { printf '%s\n' "$*"; }
|
||||
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
|
||||
@@ -39,6 +42,42 @@ volume="moshiro-$ROLE-config"
|
||||
linux=0
|
||||
[ "$(uname -s)" = Linux ] && linux=1
|
||||
|
||||
if [ "${MOSHIRO_DRY_RUN:-}" != 1 ] && eng inspect "$container" >/dev/null 2>&1 </dev/null; then
|
||||
eng start "$container" >/dev/null </dev/null
|
||||
say "$container is already installed and running."
|
||||
say "reinstall: $engine rm -f $container && $engine volume rm $volume (then run the install command again)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 验发布清单:签名对不上、组件不对、digest 格式不对都直接拒绝,不装
|
||||
resolve_image() {
|
||||
if [ "${MOSHIRO_DRY_RUN:-}" = 1 ]; then return 0; fi
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
have_manifest=1
|
||||
curl -fsS "$SERVER/install/manifest?code=$CODE" -o "$tmp/manifest" 2>/dev/null </dev/null || have_manifest=0
|
||||
curl -fsS "$SERVER/install/manifest.sig?code=$CODE" -o "$tmp/sig.b64" 2>/dev/null </dev/null || have_manifest=0
|
||||
if [ "$have_manifest" -eq 0 ]; then
|
||||
[ "${MOSHIRO_ALLOW_UNVERIFIED:-}" = 1 ] || die "the panel has no signed release for $ROLE yet, so the image cannot be verified. Run again with MOSHIRO_ALLOW_UNVERIFIED=1 to install $IMAGE without verification"
|
||||
say "warning: installing $IMAGE without signature verification"
|
||||
return 0
|
||||
fi
|
||||
command -v openssl >/dev/null 2>&1 || die "openssl is required to verify the release signature"
|
||||
printf '%s\n' "$RELEASE_KEY" > "$tmp/key.pem"
|
||||
openssl base64 -d -A -in "$tmp/sig.b64" -out "$tmp/sig.bin" 2>/dev/null || die "the release signature is not valid base64"
|
||||
openssl dgst -sha256 -verify "$tmp/key.pem" -signature "$tmp/sig.bin" "$tmp/manifest" >/dev/null 2>&1 \
|
||||
|| die "the release signature does not verify against the built-in release key; refusing to install"
|
||||
field() { sed -n "s/.*\"$1\":\"\([^\"]*\)\".*/\1/p" "$tmp/manifest" | head -n 1; }
|
||||
[ "$(field component)" = "$ROLE" ] || die "the signed release is for '$(field component)', not '$ROLE'"
|
||||
repo="$(field image)"
|
||||
digest="$(field digest)"
|
||||
case "$digest" in sha256:????????????????????????????????????????????????????????????????) ;; *) die "the signed release has a malformed digest" ;; esac
|
||||
case "$repo" in ''|*[!A-Za-z0-9._/:-]*) die "the signed release has a malformed image name" ;; esac
|
||||
IMAGE="$repo@$digest"
|
||||
say "verified release $(field version) ($(field tag)): $IMAGE"
|
||||
}
|
||||
resolve_image
|
||||
|
||||
restart=unless-stopped
|
||||
[ "$engine" = podman ] && restart=always
|
||||
set -- run -d --name "$container" --restart "$restart" \
|
||||
@@ -64,13 +103,6 @@ if [ "${MOSHIRO_DRY_RUN:-}" = 1 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if eng inspect "$container" >/dev/null 2>&1 </dev/null; then
|
||||
eng start "$container" >/dev/null </dev/null
|
||||
say "$container is already installed and running."
|
||||
say "reinstall: $engine rm -f $container && $engine volume rm $volume (then run the install command again)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
say "pulling $IMAGE"
|
||||
if ! eng pull "$IMAGE" >/dev/null </dev/null; then
|
||||
die "could not pull $IMAGE (private registry? run: $engine login ${IMAGE%%/*})"
|
||||
|
||||
Reference in new issue
Block a user