#!/bin/bash # cp-harness init — CPH-4/5/6 startup verification. # # Verifies five dispatcher-injected files exist with correct modes, # validates cp_url (https:// prefix, no whitespace/newlines), and checks # the not_after RFC 3339 timestamp is not expired. # # CPH-4: file existence + mode checks (tls.key must be 0400) # CPH-5: cp_url must start with https://, no whitespace/newlines # CPH-6: not_after must be a valid RFC 3339 timestamp in the future # # RUN_DIR: defaults to /run; tests pass a tmpdir path via env. # PROFILE_D_DIR: defaults to /etc/profile.d; override in tests if needed. set -euo pipefail RUN_DIR="${RUN_DIR:-/run}" PROFILE_D_DIR="${PROFILE_D_DIR:-/etc/profile.d}" # --------------------------------------------------------------------------- # CPH-4: verify file existence and modes # --------------------------------------------------------------------------- TLS_CRT="$RUN_DIR/cp-client/tls.crt" TLS_KEY="$RUN_DIR/cp-client/tls.key" CA_CRT="$RUN_DIR/cp-client/ca.crt" CP_URL_FILE="$RUN_DIR/cp-harness/cp_url" NOT_AFTER_FILE="$RUN_DIR/cp-harness/not_after" # Check all five files exist and are readable. for f in "$TLS_CRT" "$TLS_KEY" "$CA_CRT" "$CP_URL_FILE" "$NOT_AFTER_FILE"; do if [ ! -r "$f" ]; then echo "ERROR: missing required file $f" >&2 exit 1 fi done # tls.key must be strictly 0400 (private key — defence in depth). key_mode=$(stat -c %a "$TLS_KEY") if [ "$key_mode" != "400" ]; then # Attempt to tighten the mode. if ! chmod 0400 "$TLS_KEY" 2>/tmp/cp_harness_chmod_err; then chmod_err=$(cat /tmp/cp_harness_chmod_err 2>/dev/null || true) echo "ERROR: tls.key mode $key_mode is broader than 0400; chmod failed: $chmod_err" >&2 exit 1 fi # Re-check after chmod. key_mode=$(stat -c %a "$TLS_KEY") if [ "$key_mode" != "400" ]; then echo "ERROR: tls.key mode $key_mode remains broader than 0400 after chmod" >&2 exit 1 fi fi # --------------------------------------------------------------------------- # CPH-5: validate cp_url # --------------------------------------------------------------------------- cp_url=$(cat "$CP_URL_FILE") # Must start with https:// (case-sensitive, literal). if [[ "$cp_url" != https://* ]]; then echo "ERROR: cp_url does not start with https:// prefix" >&2 exit 1 fi # Must not contain carriage return, newline, or any whitespace. # Use explicit byte checks plus [[:space:]] guard. if printf '%s' "$cp_url" | grep -qP '\r|\n'; then echo "ERROR: cp_url contains invalid whitespace/newline" >&2 exit 1 fi if [[ "$cp_url" =~ [[:space:]] ]]; then echo "ERROR: cp_url contains invalid whitespace/newline" >&2 exit 1 fi # Export for downstream processes. Fail silently if /etc/profile.d is unwritable # (test environments may not have it). mkdir -p "$PROFILE_D_DIR" 2>/dev/null || true printf 'export CP_URL=%s\n' "$cp_url" > "$PROFILE_D_DIR/cp-url.sh" 2>/dev/null || true # --------------------------------------------------------------------------- # CPH-6: validate not_after RFC 3339 timestamp # --------------------------------------------------------------------------- not_after=$(cat "$NOT_AFTER_FILE") # Parse and validate: exit 1 if expired or unparseable. if ! python3 -c " import datetime, sys raw = sys.argv[1].strip() try: t = datetime.datetime.fromisoformat(raw.rstrip('Z').replace('Z', '+00:00')) if t.tzinfo is None: t = t.replace(tzinfo=datetime.timezone.utc) except Exception: sys.exit(1) now = datetime.datetime.now(datetime.timezone.utc) delta = (t - now).total_seconds() sys.exit(0 if delta > 0 else 1) " "$not_after" 2>/dev/null; then echo "ERROR: not_after expired or unparseable" >&2 exit 1 fi # Warn if expiry is within 300s. warn_seconds=$(python3 -c " import datetime, sys raw = sys.argv[1].strip() t = datetime.datetime.fromisoformat(raw.rstrip('Z').replace('Z', '+00:00')) if t.tzinfo is None: t = t.replace(tzinfo=datetime.timezone.utc) now = datetime.datetime.now(datetime.timezone.utc) print(int((t - now).total_seconds())) " "$not_after" 2>/dev/null || echo "0") if [ "$warn_seconds" -lt 300 ]; then echo "WARNING: cert expires in ${warn_seconds}s" >&2 fi echo "cp-harness: all checks passed"