- legacy_plantai_server_package/: phase_a–f + compile_* + bin/plantai_admin.py + tools/(audit/elf_visibility/smoke/build_delivery_manifest) 完整基线 - legacy_docker_packager/: protected 形态参考 - crypto/client/: plantai_core.py(AES-GCM 原语) + install_plantai.sh 参考 - secrets/provision 严格排除(gitignore),入 git 前已脱敏清理 - LEGACY_MAPPING.md 记录源与改造方向 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
130 lines
4.8 KiB
Bash
Executable File
130 lines
4.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Run this on your server, once per customer device, AFTER that customer has
|
|
# sent you their device_fingerprint.json (produced by the client package's
|
|
# install_plantai.sh fingerprint step).
|
|
#
|
|
# Usage:
|
|
# ./issue_client.sh \
|
|
# --fingerprint /path/to/device_fingerprint.json \
|
|
# --release-version 1.2.0 \
|
|
# --customer-id CUS-0001 \
|
|
# --customer-name "Some Customer Co" \
|
|
# --serial-no PlantAI-0001
|
|
#
|
|
# Produces: out/provision/<device_id>/license.json and asset.key.enc,
|
|
# packaged into out/provision/plantai-provision-<device_id>.tar.gz
|
|
# which is the ONLY thing you send back to that customer.
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
for cfg in "$SCRIPT_DIR/server.env"; do
|
|
[ -f "$cfg" ] && . "$cfg"
|
|
done
|
|
|
|
SECRETS_DIR="${SECRETS_DIR:-$SCRIPT_DIR/secrets}"
|
|
ADMIN="$SCRIPT_DIR/bin/plantai_admin.py"
|
|
PYTHON="${PLANTAI_PYTHON:-python3}"
|
|
|
|
KEY_PASSPHRASE_ARGS=()
|
|
if [ -n "${SUPPLIER_KEY_PASSPHRASE_ENV:-}" ] && [ -n "${SUPPLIER_KEY_PASSPHRASE_FILE:-}" ]; then
|
|
echo "Set only one of SUPPLIER_KEY_PASSPHRASE_ENV or SUPPLIER_KEY_PASSPHRASE_FILE" >&2
|
|
exit 1
|
|
elif [ -n "${SUPPLIER_KEY_PASSPHRASE_ENV:-}" ]; then
|
|
KEY_PASSPHRASE_ARGS=(--key-passphrase-env "$SUPPLIER_KEY_PASSPHRASE_ENV")
|
|
elif [ -n "${SUPPLIER_KEY_PASSPHRASE_FILE:-}" ]; then
|
|
KEY_PASSPHRASE_ARGS=(--key-passphrase-file "$SUPPLIER_KEY_PASSPHRASE_FILE")
|
|
fi
|
|
|
|
FINGERPRINT=""
|
|
RELEASE_VERSION=""
|
|
CUSTOMER_ID=""
|
|
CUSTOMER_NAME=""
|
|
SERIAL_NO=""
|
|
EDITION="${EDITION:-standard}"
|
|
MIN_MATCH_SCORE="${MIN_MATCH_SCORE:-60}"
|
|
ALLOW_WARNING_SCORE="${ALLOW_WARNING_SCORE:-85}"
|
|
ALLOWED_ASSETS="${ALLOWED_ASSETS:-runtime-assets}"
|
|
ALLOW_UNVERIFIED_TPM_KEY=""
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--fingerprint) FINGERPRINT="$2"; shift 2 ;;
|
|
--release-version) RELEASE_VERSION="$2"; shift 2 ;;
|
|
--customer-id) CUSTOMER_ID="$2"; shift 2 ;;
|
|
--customer-name) CUSTOMER_NAME="$2"; shift 2 ;;
|
|
--serial-no) SERIAL_NO="$2"; shift 2 ;;
|
|
--edition) EDITION="$2"; shift 2 ;;
|
|
--min-match-score) MIN_MATCH_SCORE="$2"; shift 2 ;;
|
|
--allow-warning-score) ALLOW_WARNING_SCORE="$2"; shift 2 ;;
|
|
--allowed-assets) ALLOWED_ASSETS="$2"; shift 2 ;;
|
|
--allow-unverified-tpm-key) ALLOW_UNVERIFIED_TPM_KEY="--allow-unverified-tpm-key"; shift ;;
|
|
*) echo "Unknown arg: $1" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
for req in FINGERPRINT RELEASE_VERSION CUSTOMER_ID CUSTOMER_NAME SERIAL_NO; do
|
|
if [ -z "${!req}" ]; then
|
|
echo "Missing required --${req,,}" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
MASTER_KEY="$SECRETS_DIR/master_keys/$RELEASE_VERSION.key"
|
|
if [ ! -f "$MASTER_KEY" ]; then
|
|
echo "No master key found for release $RELEASE_VERSION at $MASTER_KEY -- run build_release.sh first." >&2
|
|
exit 1
|
|
fi
|
|
|
|
DEVICE_ID="$("$PYTHON" -c "import json,sys; print(json.load(open(sys.argv[1]))['device_id'])" "$FINGERPRINT")"
|
|
OUT_DIR="$SCRIPT_DIR/out/provision/$DEVICE_ID"
|
|
mkdir -p "$OUT_DIR"
|
|
|
|
echo "[1/3] Log device registration"
|
|
mkdir -p "$SCRIPT_DIR/out/registry"
|
|
"$PYTHON" - "$FINGERPRINT" "$CUSTOMER_ID" "$DEVICE_ID" "$RELEASE_VERSION" >> "$SCRIPT_DIR/out/registry/devices.jsonl" <<'PYEOF'
|
|
import json, sys, datetime
|
|
fp_path, customer_id, device_id, release_version = sys.argv[1:5]
|
|
fp = json.load(open(fp_path))
|
|
print(json.dumps({
|
|
"at": datetime.datetime.now().astimezone().isoformat(timespec="seconds"),
|
|
"device_id": device_id,
|
|
"customer_id": customer_id,
|
|
"release_version": release_version,
|
|
"core_id_anchor": fp.get("core_id_anchor"),
|
|
"core_id_hash": fp.get("core_id_hash"),
|
|
}, ensure_ascii=False))
|
|
PYEOF
|
|
|
|
# IMPORTANT: wrap-key-for-device runs FIRST, then issue-license with
|
|
# --asset-key-envelope so the license contains a cryptographic binding
|
|
# (asset_key_sha256) to the exact asset.key.enc. The client will
|
|
# verify this hash at runtime to detect tampering or mismatched files.
|
|
echo "[2/3] Wrap this release's K_master for this device"
|
|
"$PYTHON" "$ADMIN" wrap-key-for-device \
|
|
--fingerprint "$FINGERPRINT" \
|
|
--master-key "$MASTER_KEY" \
|
|
--output "$OUT_DIR/asset.key.enc" \
|
|
$ALLOW_UNVERIFIED_TPM_KEY
|
|
|
|
echo "[3/3] Issue signed license (bound to asset.key.enc hash)"
|
|
"$PYTHON" "$ADMIN" issue-license \
|
|
--fingerprint "$FINGERPRINT" \
|
|
--private-key "$SECRETS_DIR/supplier_private_key.pem" \
|
|
--output "$OUT_DIR/license.json" \
|
|
--customer-id "$CUSTOMER_ID" \
|
|
--customer-name "$CUSTOMER_NAME" \
|
|
--serial-no "$SERIAL_NO" \
|
|
--edition "$EDITION" \
|
|
--release-version "$RELEASE_VERSION" \
|
|
--min-match-score "$MIN_MATCH_SCORE" \
|
|
--allow-warning-score "$ALLOW_WARNING_SCORE" \
|
|
--allowed-assets $ALLOWED_ASSETS \
|
|
--asset-key-envelope "$OUT_DIR/asset.key.enc" \
|
|
"${KEY_PASSPHRASE_ARGS[@]}"
|
|
|
|
tar -C "$OUT_DIR" -czf "$SCRIPT_DIR/out/provision/plantai-provision-$DEVICE_ID.tar.gz" license.json asset.key.enc
|
|
|
|
echo "Done."
|
|
echo "Send this file back to the customer: $SCRIPT_DIR/out/provision/plantai-provision-$DEVICE_ID.tar.gz"
|
|
echo "It contains only license.json + asset.key.enc -- no keys, no master key."
|