446 lines
13 KiB
Bash
446 lines
13 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
LICENSE_DIR="/etc/saikyo/license"
|
|
PUBKEY_FILE="/usr/share/saikyo-os/license/saikyo-license-pub.pem"
|
|
PUBKEY_ED25519_FILE="/usr/share/saikyo-os/license/saikyo-license-ed25519-pub.pem"
|
|
LICENSE_JSON="${LICENSE_DIR}/license.json"
|
|
LICENSE_SIG="${LICENSE_DIR}/license.sig"
|
|
TRIAL_START_FILE="${LICENSE_DIR}/trial_start"
|
|
TRIAL_DAYS=7
|
|
|
|
require_jq() {
|
|
if ! command -v jq >/dev/null 2>&1; then
|
|
echo "ERROR: jq is required" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
usage() {
|
|
echo "Usage: saikyo-license <command> [args]" >&2
|
|
echo "Commands:" >&2
|
|
echo " install <license.json> <license.sig>" >&2
|
|
echo " install-code <response-code>" >&2
|
|
echo " activate --token <token> [--server <url>]" >&2
|
|
echo " renew --token <token> [--server <url>]" >&2
|
|
echo " verify" >&2
|
|
echo " status" >&2
|
|
exit 2
|
|
}
|
|
|
|
require_root() {
|
|
if [[ "$(id -u)" -ne 0 ]]; then
|
|
echo "ERROR: must be run as root" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
verify_pair() {
|
|
if [[ ! -f "${PUBKEY_FILE}" && ! -f "${PUBKEY_ED25519_FILE}" ]]; then
|
|
echo "ERROR: public keys not found in /usr/share/saikyo-os/license" >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! -f "${LICENSE_JSON}" || ! -f "${LICENSE_SIG}" ]]; then
|
|
if [[ -f "${TRIAL_START_FILE}" ]]; then
|
|
local start now end
|
|
start=$(cat "${TRIAL_START_FILE}" 2>/dev/null | tr -d '\r\n' | head -c 32)
|
|
now=$(date -u +%s 2>/dev/null || echo "")
|
|
if [[ -n "${start}" && -n "${now}" && "${start}" =~ ^[0-9]+$ ]]; then
|
|
end=$(( start + TRIAL_DAYS*86400 ))
|
|
if [[ "${now}" -le "${end}" ]]; then
|
|
echo "VALID"
|
|
return 0
|
|
fi
|
|
fi
|
|
fi
|
|
echo "INVALID"
|
|
return 1
|
|
fi
|
|
|
|
require_jq
|
|
|
|
# Verify signature over raw JSON bytes.
|
|
# New licenses: Ed25519 (pkeyutl -verify -rawin).
|
|
# Legacy licenses: RSA (dgst -sha256 -verify).
|
|
if [[ -f "${PUBKEY_ED25519_FILE}" ]]; then
|
|
if openssl pkeyutl -verify -pubin -inkey "${PUBKEY_ED25519_FILE}" -rawin -in "${LICENSE_JSON}" -sigfile "${LICENSE_SIG}" >/dev/null 2>&1; then
|
|
:
|
|
else
|
|
if [[ -f "${PUBKEY_FILE}" ]]; then
|
|
if ! openssl dgst -sha256 -verify "${PUBKEY_FILE}" -signature "${LICENSE_SIG}" "${LICENSE_JSON}" >/dev/null 2>&1; then
|
|
echo "INVALID"
|
|
return 1
|
|
fi
|
|
else
|
|
echo "INVALID"
|
|
return 1
|
|
fi
|
|
fi
|
|
else
|
|
if ! openssl dgst -sha256 -verify "${PUBKEY_FILE}" -signature "${LICENSE_SIG}" "${LICENSE_JSON}" >/dev/null 2>&1; then
|
|
echo "INVALID"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
local lic_machine_id lic_valid_until
|
|
lic_machine_id=$(jq -r '.machine_id // empty' "${LICENSE_JSON}" 2>/dev/null || true)
|
|
lic_valid_until=$(jq -r '.valid_until // empty' "${LICENSE_JSON}" 2>/dev/null || true)
|
|
|
|
if [[ -z "${lic_machine_id}" || -z "${lic_valid_until}" ]]; then
|
|
echo "INVALID"
|
|
return 1
|
|
fi
|
|
|
|
local current_machine_id
|
|
current_machine_id=""
|
|
if [[ -f /etc/machine-id ]]; then
|
|
current_machine_id=$(cat /etc/machine-id 2>/dev/null || true)
|
|
fi
|
|
if [[ -z "${current_machine_id}" || "${lic_machine_id}" != "${current_machine_id}" ]]; then
|
|
echo "INVALID"
|
|
return 1
|
|
fi
|
|
|
|
local now exp
|
|
now=$(date -u +%s)
|
|
exp=$(date -u -d "${lic_valid_until}" +%s 2>/dev/null || echo "")
|
|
if [[ -z "${exp}" || "${now}" -gt "${exp}" ]]; then
|
|
echo "INVALID"
|
|
return 1
|
|
fi
|
|
|
|
echo "VALID"
|
|
return 0
|
|
}
|
|
|
|
cmd_install() {
|
|
require_root
|
|
local src_json="${1:-}"
|
|
local src_sig="${2:-}"
|
|
[[ -n "${src_json}" && -n "${src_sig}" ]] || usage
|
|
[[ -f "${src_json}" ]] || { echo "ERROR: file not found: ${src_json}" >&2; exit 1; }
|
|
[[ -f "${src_sig}" ]] || { echo "ERROR: file not found: ${src_sig}" >&2; exit 1; }
|
|
|
|
require_jq
|
|
|
|
local lic_machine_id
|
|
lic_machine_id=$(jq -r '.machine_id // empty' "${src_json}" 2>/dev/null || true)
|
|
if [[ -z "${lic_machine_id}" ]]; then
|
|
echo "ERROR: license.json missing required field: machine_id" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local current_machine_id
|
|
current_machine_id=""
|
|
if [[ -f /etc/machine-id ]]; then
|
|
current_machine_id=$(cat /etc/machine-id 2>/dev/null || true)
|
|
fi
|
|
if [[ -z "${current_machine_id}" ]]; then
|
|
echo "ERROR: cannot read /etc/machine-id" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "${lic_machine_id}" != "${current_machine_id}" ]]; then
|
|
echo "ERROR: machine-id mismatch" >&2
|
|
echo " expected=${lic_machine_id}" >&2
|
|
echo " actual=${current_machine_id}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
mkdir -p "${LICENSE_DIR}"
|
|
chmod 0755 "${LICENSE_DIR}"
|
|
install -m 0644 "${src_json}" "${LICENSE_JSON}"
|
|
install -m 0644 "${src_sig}" "${LICENSE_SIG}"
|
|
|
|
verify_pair >/dev/null
|
|
echo "OK"
|
|
}
|
|
|
|
cmd_install_code() {
|
|
require_root
|
|
require_jq
|
|
|
|
local rc="${1:-}"
|
|
[[ -n "${rc}" ]] || usage
|
|
rc=$(printf '%s' "$rc" | tr -d '\r\n\t ' | tr -d '-' | head -c 20000)
|
|
|
|
local td lic_json lic_sig
|
|
td=$(mktemp -d)
|
|
lic_json="${td}/license.json"
|
|
lic_sig="${td}/license.sig"
|
|
|
|
if printf '%s' "$rc" | grep -q '^SAI-RC:'; then
|
|
if ! command -v base32 >/dev/null 2>&1; then
|
|
echo "ERROR: base32 is required" >&2
|
|
rm -rf "${td}" || true
|
|
exit 1
|
|
fi
|
|
local body ppart spart ppad spad
|
|
body=${rc#SAI-RC:}
|
|
ppart=${body%%.*}
|
|
spart=${body#*.}
|
|
if [[ -z "${ppart}" || -z "${spart}" || "${ppart}" == "${body}" ]]; then
|
|
echo "ERROR: invalid SAI-RC format" >&2
|
|
rm -rf "${td}" || true
|
|
exit 1
|
|
fi
|
|
ppad=$(( (8 - (${#ppart} % 8)) % 8 ))
|
|
spad=$(( (8 - (${#spart} % 8)) % 8 ))
|
|
if [[ "$ppad" -gt 0 ]]; then ppart="${ppart}$(printf '%*s' "$ppad" '' | tr ' ' '=')"; fi
|
|
if [[ "$spad" -gt 0 ]]; then spart="${spart}$(printf '%*s' "$spad" '' | tr ' ' '=')"; fi
|
|
printf '%s' "$ppart" | base32 -d >"${lic_json}" 2>/dev/null || true
|
|
printf '%s' "$spart" | base32 -d >"${lic_sig}" 2>/dev/null || true
|
|
elif printf '%s' "$rc" | grep -q '^SAI-RESP2:'; then
|
|
local body jpart spart jb64 sb64 jpad spad
|
|
body=${rc#SAI-RESP2:}
|
|
jpart=${body%%.*}
|
|
spart=${body#*.}
|
|
if [[ -z "${jpart}" || -z "${spart}" || "${jpart}" == "${body}" ]]; then
|
|
echo "ERROR: invalid SAI-RESP2 format" >&2
|
|
rm -rf "${td}" || true
|
|
exit 1
|
|
fi
|
|
jb64=$(printf '%s' "$jpart" | tr '_-' '/+')
|
|
sb64=$(printf '%s' "$spart" | tr '_-' '/+')
|
|
jpad=$(( (4 - (${#jb64} % 4)) % 4 ))
|
|
spad=$(( (4 - (${#sb64} % 4)) % 4 ))
|
|
if [[ "$jpad" -eq 1 ]]; then jb64="${jb64}="; elif [[ "$jpad" -eq 2 ]]; then jb64="${jb64}=="; elif [[ "$jpad" -eq 3 ]]; then jb64="${jb64}==="; fi
|
|
if [[ "$spad" -eq 1 ]]; then sb64="${sb64}="; elif [[ "$spad" -eq 2 ]]; then sb64="${sb64}=="; elif [[ "$spad" -eq 3 ]]; then sb64="${sb64}==="; fi
|
|
printf '%s' "$jb64" | base64 -d >"${lic_json}" 2>/dev/null || true
|
|
printf '%s' "$sb64" | base64 -d >"${lic_sig}" 2>/dev/null || true
|
|
elif printf '%s' "$rc" | grep -q '^SAI-RESP:'; then
|
|
local b64u payload lic_json_b64 lic_sig_b64 pad
|
|
b64u=${rc#SAI-RESP:}
|
|
b64u=$(printf '%s' "$b64u" | tr '_-' '/+')
|
|
pad=$(( (4 - (${#b64u} % 4)) % 4 ))
|
|
if [[ "$pad" -eq 1 ]]; then b64u="${b64u}="; elif [[ "$pad" -eq 2 ]]; then b64u="${b64u}=="; elif [[ "$pad" -eq 3 ]]; then b64u="${b64u}==="; fi
|
|
payload=$(printf '%s' "$b64u" | base64 -d 2>/dev/null || true)
|
|
lic_json_b64=$(printf '%s' "$payload" | sed -n 's/.*"license_json_b64":"\([^"]*\)".*/\1/p' | head -c 20000)
|
|
lic_sig_b64=$(printf '%s' "$payload" | sed -n 's/.*"license_sig_b64":"\([^"]*\)".*/\1/p' | head -c 20000)
|
|
if [[ -z "${lic_json_b64}" || -z "${lic_sig_b64}" ]]; then
|
|
echo "ERROR: invalid SAI-RESP payload" >&2
|
|
rm -rf "${td}" || true
|
|
exit 1
|
|
fi
|
|
printf '%s' "$lic_json_b64" | base64 -d >"${lic_json}" 2>/dev/null || true
|
|
printf '%s' "$lic_sig_b64" | base64 -d >"${lic_sig}" 2>/dev/null || true
|
|
else
|
|
echo "ERROR: unsupported response-code format" >&2
|
|
rm -rf "${td}" || true
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -s "${lic_json}" || ! -s "${lic_sig}" ]]; then
|
|
echo "ERROR: failed to decode response-code" >&2
|
|
rm -rf "${td}" || true
|
|
exit 1
|
|
fi
|
|
|
|
cmd_install "${lic_json}" "${lic_sig}"
|
|
rm -rf "${td}" || true
|
|
}
|
|
|
|
cmd_verify() {
|
|
verify_pair
|
|
}
|
|
|
|
cmd_activate() {
|
|
require_root
|
|
require_jq
|
|
|
|
local server="https://saikyo-os.ru/license"
|
|
local token=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--server)
|
|
server="${2:-}"; shift 2 || true
|
|
;;
|
|
--token)
|
|
token="${2:-}"; shift 2 || true
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
;;
|
|
*)
|
|
echo "ERROR: unknown option: $1" >&2
|
|
usage
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "${token}" ]]; then
|
|
echo "ERROR: --token is required" >&2
|
|
exit 1
|
|
fi
|
|
if [[ -z "${server}" ]]; then
|
|
echo "ERROR: --server is empty" >&2
|
|
exit 1
|
|
fi
|
|
if ! command -v curl >/dev/null 2>&1; then
|
|
echo "ERROR: curl is required for online activation" >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! -r /etc/ssl/certs/ca-certificates.crt ]]; then
|
|
echo "ERROR: CA bundle not found: /etc/ssl/certs/ca-certificates.crt" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local server_host
|
|
server_host=$(printf '%s' "${server}" | sed -n 's#^https\?://\([^/]*\).*#\1#p' | head -n 1)
|
|
if [[ -n "${server_host}" ]] && command -v getent >/dev/null 2>&1; then
|
|
if ! getent ahosts "${server_host}" >/dev/null 2>&1; then
|
|
echo "ERROR: cannot resolve server host: ${server_host}" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
local mid
|
|
mid=$(cat /etc/machine-id 2>/dev/null || true)
|
|
if [[ -z "${mid}" ]]; then
|
|
echo "ERROR: cannot read /etc/machine-id" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local req tmpdir resp_json lic_json_b64 lic_sig_b64
|
|
req=$(jq -n --arg token "${token}" --arg machine_id "${mid}" --arg hostname "$(hostname 2>/dev/null || true)" '{token:$token, machine_id:$machine_id, hostname:$hostname}')
|
|
|
|
tmpdir=$(mktemp -d)
|
|
trap 'rm -rf "${tmpdir}" 2>/dev/null || true' RETURN
|
|
|
|
resp_json="${tmpdir}/response.json"
|
|
resp_err="${tmpdir}/curl.stderr"
|
|
req_file="${tmpdir}/request.json"
|
|
http_code=""
|
|
|
|
printf '%s' "${req}" > "${req_file}"
|
|
|
|
http_code=$(curl -sS \
|
|
--connect-timeout 10 \
|
|
--max-time 30 \
|
|
--retry 3 \
|
|
--retry-delay 1 \
|
|
--retry-connrefused \
|
|
--retry-all-errors \
|
|
-H 'Content-Type: application/json' \
|
|
-X POST \
|
|
--data-binary "@${req_file}" \
|
|
"${server%/}/v1/activate" \
|
|
-o "${resp_json}" \
|
|
-w '%{http_code}' \
|
|
2>"${resp_err}" || true)
|
|
|
|
if [[ "${http_code}" != 2* ]]; then
|
|
echo "ERROR: activation request failed (http_code=${http_code:-unknown})" >&2
|
|
if [[ -s "${resp_err}" ]]; then
|
|
sed -n '1,120p' "${resp_err}" >&2 || true
|
|
fi
|
|
if [[ -s "${resp_json}" ]]; then
|
|
sed -n '1,200p' "${resp_json}" >&2 || true
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
lic_json_b64=$(jq -r '.license_json_b64 // empty' "${resp_json}" 2>/dev/null || true)
|
|
lic_sig_b64=$(jq -r '.license_sig_b64 // empty' "${resp_json}" 2>/dev/null || true)
|
|
|
|
if [[ -z "${lic_json_b64}" || -z "${lic_sig_b64}" ]]; then
|
|
echo "ERROR: invalid response from license server" >&2
|
|
cat "${resp_json}" >&2 || true
|
|
exit 1
|
|
fi
|
|
|
|
local lic_json_file lic_sig_file
|
|
lic_json_file="${tmpdir}/license.json"
|
|
lic_sig_file="${tmpdir}/license.sig"
|
|
|
|
printf '%s' "${lic_json_b64}" | base64 -d > "${lic_json_file}"
|
|
printf '%s' "${lic_sig_b64}" | base64 -d > "${lic_sig_file}"
|
|
|
|
cmd_install "${lic_json_file}" "${lic_sig_file}"
|
|
}
|
|
|
|
cmd_renew() {
|
|
cmd_activate "$@"
|
|
}
|
|
|
|
cmd_status() {
|
|
if [[ ! -f "${LICENSE_JSON}" ]]; then
|
|
if [[ -f "${TRIAL_START_FILE}" ]]; then
|
|
local start now end rem
|
|
start=$(cat "${TRIAL_START_FILE}" 2>/dev/null | tr -d '\r\n' | head -c 32)
|
|
now=$(date -u +%s 2>/dev/null || echo "")
|
|
if [[ -n "${start}" && -n "${now}" && "${start}" =~ ^[0-9]+$ ]]; then
|
|
end=$(( start + TRIAL_DAYS*86400 ))
|
|
if [[ "${now}" -le "${end}" ]]; then
|
|
rem=$(( end - now ))
|
|
echo "status=trial"
|
|
echo "trial_days=${TRIAL_DAYS}"
|
|
echo "trial_remaining_seconds=${rem}"
|
|
exit 0
|
|
fi
|
|
fi
|
|
fi
|
|
echo "status=missing"
|
|
exit 0
|
|
fi
|
|
|
|
local sig_status
|
|
if sig_status=$(verify_pair 2>/dev/null); then
|
|
echo "signature=${sig_status,,}"
|
|
else
|
|
echo "signature=invalid"
|
|
fi
|
|
|
|
require_jq
|
|
|
|
local org_id org_name valid_until tier machine_id
|
|
org_id=$(jq -r '.org_id // empty' "${LICENSE_JSON}" 2>/dev/null || true)
|
|
org_name=$(jq -r '.org_name // empty' "${LICENSE_JSON}" 2>/dev/null || true)
|
|
valid_until=$(jq -r '.valid_until // empty' "${LICENSE_JSON}" 2>/dev/null || true)
|
|
tier=$(jq -r '.tier // empty' "${LICENSE_JSON}" 2>/dev/null || true)
|
|
machine_id=$(jq -r '.machine_id // empty' "${LICENSE_JSON}" 2>/dev/null || true)
|
|
|
|
[[ -n "${org_id}" ]] && echo "org_id=${org_id}"
|
|
[[ -n "${org_name}" ]] && echo "org_name=${org_name}"
|
|
[[ -n "${tier}" ]] && echo "tier=${tier}"
|
|
[[ -n "${machine_id}" ]] && echo "machine_id=${machine_id}"
|
|
[[ -n "${valid_until}" ]] && echo "valid_until=${valid_until}"
|
|
|
|
if [[ -n "${valid_until}" ]]; then
|
|
local now exp
|
|
now=$(date -u +%s)
|
|
exp=$(date -u -d "${valid_until}" +%s 2>/dev/null || echo "")
|
|
if [[ -n "${exp}" && "${now}" -gt "${exp}" ]]; then
|
|
echo "expired=yes"
|
|
else
|
|
echo "expired=no"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
local cmd="${1:-}"
|
|
shift || true
|
|
case "${cmd}" in
|
|
install)
|
|
cmd_install "$@"
|
|
;;
|
|
install-code)
|
|
cmd_install_code "$@"
|
|
;;
|
|
activate)
|
|
cmd_activate "$@"
|
|
;;
|
|
renew)
|
|
cmd_renew "$@"
|
|
;;
|
|
verify) cmd_verify;;
|
|
status) cmd_status;;
|
|
-h|--help|help|"") usage;;
|
|
*) usage;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|