#!/bin/sh set -eu log() { printf '[shellorchestra-verify] %s\n' "$*"; } fatal() { printf '[shellorchestra-verify] ERROR: %s\n' "$*" >&2; exit 1; } require_command() { command -v "$1" >/dev/null 2>&1 || fatal "Required command is missing: $1"; } MANIFEST_URL="${SHELLORCHESTRA_RELEASE_MANIFEST_URL:-https://shellorchestra.com/releases/mainstream.json}" KEYRING_URL="${SHELLORCHESTRA_RELEASE_KEYRING_URL:-https://shellorchestra.com/releases/keyring.json}" EMBEDDED_RELEASE_ROOT_PUBLIC_KEY_PEM_B64='LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUNvd0JRWURLMlZ3QXlFQXZZSVNRY0VLdVhTUkRoOEtZeXBxOE9jMWxyZTUzQW56OTBkamphUXJZUzg9Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo=' RELEASE_ROOT_PUBLIC_KEY_PEM_B64="${SHELLORCHESTRA_RELEASE_ROOT_PUBLIC_KEY_PEM_B64:-$EMBEDDED_RELEASE_ROOT_PUBLIC_KEY_PEM_B64}" CHANNEL="${SHELLORCHESTRA_UPDATE_CHANNEL:-}" ARTIFACT_NAME="${SHELLORCHESTRA_VERIFY_ARTIFACT_NAME:-docker}" ARTIFACT_FILE="${SHELLORCHESTRA_VERIFY_ARTIFACT_FILE:-}" ALLOW_FILE_URLS="${SHELLORCHESTRA_VERIFY_ALLOW_FILE_URLS:-0}" validate_release_download_url() { label="$1" value="$2" case "$value" in https://*) return 0 ;; file://*) [ "$ALLOW_FILE_URLS" = "1" ] || fatal "$label file:// URLs are disabled. Set SHELLORCHESTRA_VERIFY_ALLOW_FILE_URLS=1 only for an explicit local/offline verification test." return 0 ;; *) fatal "$label URL must use https://." ;; esac } case "$CHANNEL" in "") ;; *[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-]*) fatal "ShellOrchestra update channel must contain only letters, digits, dot, underscore, or dash." ;; esac case "$ARTIFACT_NAME" in ""|*[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-]*) fatal "Artifact name must contain only letters, digits, dot, underscore, or dash." ;; esac validate_release_download_url "Manifest" "$MANIFEST_URL" validate_release_download_url "Release keyring" "$KEYRING_URL" if [ -n "$ARTIFACT_FILE" ] && [ ! -f "$ARTIFACT_FILE" ]; then fatal "Artifact file is not readable: $ARTIFACT_FILE" fi case "$RELEASE_ROOT_PUBLIC_KEY_PEM_B64" in ''|__SHELLORCHESTRA_RELEASE_ROOT_PUBLIC_KEY_PEM_B64__) fatal "verify-release.sh has not been stamped with the ShellOrchestra release root public key. Use the official script from https://shellorchestra.com/install/verify-release.sh or configure SHELLORCHESTRA_RELEASE_ROOT_PUBLIC_KEY_PEM_B64 explicitly for an audited offline verification." ;; esac require_command curl require_command openssl require_command python3 require_command sha256sum require_command base64 require_command sed require_command awk require_command grep require_command mktemp b64decode() { if base64 --help 2>/dev/null | grep -q -- '--decode'; then base64 --decode else base64 -d fi } verify_ed25519_signature() { key_file="$1" payload_file="$2" signature_b64="$3" label="$4" sig_file="$WORK_DIR/$label.sig" printf '%s' "$signature_b64" | b64decode > "$sig_file" || fatal "Could not decode $label signature." openssl pkeyutl -verify -pubin -inkey "$key_file" -rawin -in "$payload_file" -sigfile "$sig_file" >/dev/null 2>&1 || fatal "$label signature verification failed." } verify_release_signature() { payload_file="$1" signature_b64="$2" label="$3" verify_ed25519_signature "$WORK_DIR/release-public-key.pem" "$payload_file" "$signature_b64" "$label" } WORK_DIR="$(mktemp -d)" cleanup() { rm -rf "$WORK_DIR"; } trap cleanup EXIT INT TERM printf '%s' "$RELEASE_ROOT_PUBLIC_KEY_PEM_B64" | b64decode > "$WORK_DIR/release-root-public-key.pem" || fatal "Could not decode the embedded release root public key." log "Downloading ShellOrchestra signed release keyring from $KEYRING_URL." curl -fsSL --max-time 30 "$KEYRING_URL" -o "$WORK_DIR/keyring.json" || fatal "Could not download release keyring." python3 - "$WORK_DIR/keyring.json" "$WORK_DIR/keyring-signed.json" "$WORK_DIR/keyring.env" <<'PY' import base64 import json import shlex import sys from pathlib import Path keyring_path, signed_path, env_path = map(Path, sys.argv[1:]) keyring = json.loads(keyring_path.read_text(encoding='utf-8')) if keyring.get('schema') != 'shellorchestra.release-keyring.v1': raise SystemExit('release keyring schema is not supported') signed_b64 = keyring.get('signed_payload_b64') signature = keyring.get('signature', {}).get('value') if not isinstance(signed_b64, str) or not signed_b64: raise SystemExit('release keyring does not include signed_payload_b64') if not isinstance(signature, str) or not signature: raise SystemExit('release keyring does not include signature.value') payload = base64.b64decode(signed_b64, validate=True) signed_path.write_bytes(payload) signed = json.loads(payload.decode('utf-8')) if signed.get('product') != 'ShellOrchestra': raise SystemExit('release keyring product is not ShellOrchestra') with env_path.open('w', encoding='utf-8') as out: out.write(f'KEYRING_SIGNATURE_B64={shlex.quote(signature)}\n') PY . "$WORK_DIR/keyring.env" verify_ed25519_signature "$WORK_DIR/release-root-public-key.pem" "$WORK_DIR/keyring-signed.json" "$KEYRING_SIGNATURE_B64" keyring log "Downloading signed release manifest from $MANIFEST_URL." curl -fsSL --max-time 30 "$MANIFEST_URL" -o "$WORK_DIR/manifest.json" || fatal "Could not download release manifest." python3 - "$WORK_DIR/keyring.json" "$WORK_DIR/manifest.json" "$WORK_DIR/release-public-key.pem" "$WORK_DIR/signed.json" "$WORK_DIR/metadata.env" "$ARTIFACT_NAME" <<'PY' import base64 import json import shlex import sys from datetime import datetime, timezone from pathlib import Path keyring_path, manifest_path, release_key_pem_path, signed_path, env_path = map(Path, sys.argv[1:6]) artifact_name = sys.argv[6] keyring = json.loads(keyring_path.read_text(encoding='utf-8')) keyring_signed = json.loads(base64.b64decode(keyring['signed_payload_b64'], validate=True).decode('utf-8')) now = datetime.now(timezone.utc) expires_at = datetime.fromisoformat(str(keyring_signed.get('expires_at')).replace('Z', '+00:00')) if now >= expires_at: raise SystemExit('release keyring has expired') revoked = set(keyring_signed.get('revoked_key_ids') or []) manifest = json.loads(manifest_path.read_text(encoding='utf-8')) signed_b64 = manifest.get('signed_payload_b64') signature = manifest.get('signature', {}).get('value') key_id = manifest.get('signature', {}).get('key_id', '') if not isinstance(signed_b64, str) or not signed_b64: raise SystemExit('release manifest does not include signed_payload_b64') if not isinstance(signature, str) or not signature: raise SystemExit('release manifest does not include signature.value') payload = base64.b64decode(signed_b64, validate=True) signed_path.write_bytes(payload) signed = json.loads(payload.decode('utf-8')) if signed.get('product') != 'ShellOrchestra': raise SystemExit('release manifest product is not ShellOrchestra') channel = signed.get('channel') release_public_key = None for key in keyring_signed.get('release_keys') or []: if key.get('key_id') != key_id or key_id in revoked: continue channels = key.get('channels') or [] if channels and channel not in channels: continue valid_from = datetime.fromisoformat(str(key.get('valid_from')).replace('Z', '+00:00')) valid_until = datetime.fromisoformat(str(key.get('valid_until')).replace('Z', '+00:00')) if now < valid_from or now >= valid_until: continue release_public_key = key.get('public_key') break if not isinstance(release_public_key, str): raise SystemExit(f'release keyring has no active key for {key_id!r} and channel {channel!r}') if release_public_key.startswith('ed25519:'): release_public_key = release_public_key[len('ed25519:'):] raw_public = base64.b64decode(release_public_key, validate=True) if len(raw_public) != 32: raise SystemExit('release public key in keyring is not a raw Ed25519 public key') der = bytes([0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00]) + raw_public body = base64.encodebytes(der).decode('ascii') release_key_pem_path.write_text('-----BEGIN PUBLIC KEY-----\n' + body + '-----END PUBLIC KEY-----\n', encoding='ascii') artifacts = signed.get('artifacts', {}) artifact = artifacts.get(artifact_name) if not isinstance(artifact, dict): raise SystemExit(f'release manifest does not contain {artifact_name!r} artifact') values = { 'MANIFEST_SIGNATURE_B64': signature, 'KEY_ID': key_id, 'LATEST_VERSION': signed.get('latest', ''), 'SIGNED_CHANNEL': signed.get('channel', ''), 'RELEASE_NOTES_URL': signed.get('release_notes_url', ''), 'ARTIFACT_URL': artifact.get('url', ''), 'ARTIFACT_SHA256': artifact.get('sha256', ''), 'ARTIFACT_SIGNATURE_B64': artifact.get('signature', ''), } for key, value in values.items(): if key == 'KEY_ID': continue if not isinstance(value, str) or not value: raise SystemExit(f'missing manifest value: {key}') with env_path.open('w', encoding='utf-8') as out: for key, value in values.items(): out.write(f'{key}={shlex.quote(str(value))}\n') PY . "$WORK_DIR/metadata.env" if [ -n "$CHANNEL" ] && [ "$SIGNED_CHANNEL" != "$CHANNEL" ]; then fatal "Manifest channel $SIGNED_CHANNEL does not match requested channel $CHANNEL." fi case "$LATEST_VERSION" in *[!0-9.]*|.*|*.) fatal "Release manifest latest version is not a major.minor.build value: $LATEST_VERSION" ;; esac case "$RELEASE_NOTES_URL" in https://*) ;; *) fatal "Release notes URL must use https." ;; esac case "$ARTIFACT_URL" in https://*) ;; *) fatal "Artifact URL must use https." ;; esac case "$ARTIFACT_SHA256" in [0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]*) ;; *) fatal "Artifact SHA-256 is invalid." ;; esac verify_release_signature "$WORK_DIR/signed.json" "$MANIFEST_SIGNATURE_B64" manifest printf 'shellorchestra-artifact-sha256-v1\n%s\n' "$ARTIFACT_SHA256" > "$WORK_DIR/artifact-signature-payload.txt" verify_release_signature "$WORK_DIR/artifact-signature-payload.txt" "$ARTIFACT_SIGNATURE_B64" artifact if [ -n "$ARTIFACT_FILE" ]; then actual_sha="$(sha256sum "$ARTIFACT_FILE" | awk '{print $1}')" if [ "$(printf '%s' "$actual_sha" | tr 'A-F' 'a-f')" != "$(printf '%s' "$ARTIFACT_SHA256" | tr 'A-F' 'a-f')" ]; then fatal "Artifact SHA-256 mismatch for $ARTIFACT_FILE." fi log "Local artifact hash verified: $ARTIFACT_FILE." fi log "Verified signed ShellOrchestra release manifest and $ARTIFACT_NAME artifact metadata." printf 'Version: %s\n' "$LATEST_VERSION" printf 'Channel: %s\n' "$SIGNED_CHANNEL" printf 'Key id: %s\n' "${KEY_ID:-unknown}" printf 'Release notes: %s\n' "$RELEASE_NOTES_URL" printf 'Artifact URL: %s\n' "$ARTIFACT_URL" printf 'Artifact SHA-256: %s\n' "$ARTIFACT_SHA256"