saikyo-packages-src/saikyo-license/bin/saikyo-license

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 "$@"