diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b383d4..3dc8d37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,3 +27,17 @@ jobs: - name: Run Windows tests shell: pwsh run: ./testing/test_install_certs_windows.ps1 + + test-linux-jvm: + name: Test (Linux JVM) + runs-on: ubuntu-latest + # No actions/setup-java here — unlike the macOS/Windows JVM jobs that + # exercise a single host JDK, this job runs a 4-distro Docker matrix + # (Ubuntu / Debian / RHEL / Amazon Linux) and installs a JDK *inside* + # each container. The host runner doesn't need a JDK; setting one up + # would only mask divergence in the per-distro keytool versions. + steps: + - uses: actions/checkout@v4 + + - name: Run Linux JVM smoke matrix + run: ./testing/test_install_certs_jvm_linux.sh diff --git a/README.md b/README.md index 6666bcb..02fff5f 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,28 @@ Scripts to install a CA certificate, configure Node/npm and Python (pip, uv, Hug This document describes the certificate installation and validation scripts for **macOS**, **Linux (Debian/Ubuntu)**, and **Windows**. +## Quickstart — which script do I need? + +| Your toolchain | Your OS | Go to | +|---|---|---| +| **Maven / Gradle / sbt / Ivy** (JVM) | Linux | [Linux (JVM)](#linux-jvm-install_certs_jvm_linuxsh) | +| **Node / npm or Python (pip / uv / Hugging Face)** | macOS | [macOS](#macos-install_certs_macossh) | +| **Node / npm or Python** | Linux (Debian / Ubuntu) | [Linux (Debian/Ubuntu)](#linux-debianubuntu-install_certs_debian_ubuntush) | +| **Node / npm or Python** | Windows | [Windows](#windows-install_certs_windowsps1) | + +Reference: research wiki [Maven Support in package-reroute (DFLOW-136 / DFLOW-116)](https://jfrog-int.atlassian.net/wiki/spaces/RTFACT/pages/2440101931/). + +## Script index + | Script | Platform | Purpose | |--------|----------|---------| | **install_certs_macos.sh** | macOS | Install cert, set env vars (Node/Python), and clear Docker Hub credentials | | **validate_install_macos.sh** | macOS | Validate PEM and env config | | **install_certs_debian_ubuntu.sh** | Debian/Ubuntu | Install cert into system trust + profile.d + user shell rc + Docker cleanup | | **validate_certs_debian_ubuntu.sh** | Debian/Ubuntu | Validate PEM and env config | +| **install_certs_jvm_linux.sh** | Linux (JVM) | Install CA for Maven/Gradle/sbt/Ivy: RHEL family → `update-ca-trust extract` into system anchors; others → per-host JKS + `JAVA_TOOL_OPTIONS` in `/etc/environment` | +| **validate_certs_jvm_linux.sh** | Linux (JVM) | Validate JVM truststore install (auto-detects Path A vs B; checks anchor file or JKS subject + `/etc/environment` + shell-rc) | +| **_jvm_linux_paths.sh** | Linux (JVM) | Shared constants dot-sourced by installer + validator. Not directly executable. | | **install_certs_windows.ps1** | Windows | Install cert, set env vars (Node/Python), and clear Docker Hub credentials | | **validate_install_windows.ps1** | Windows | Validate PEM and env config | @@ -174,7 +190,7 @@ sudo ./validate_install_macos.sh --expected-subject Zscaler --all-users ### Testing -Tests live in **testing/**. Automated tests cover **macOS** and **Windows** only (not Debian/Ubuntu). +Tests live in **testing/**. Automated tests cover **macOS**, **Windows**, and **Linux JVM** (not Debian/Ubuntu). **test_install_certs_macos.sh** runs automated tests for **install_certs_macos.sh** (CLI and argument validation) and **validate_install_macos.sh** (validation with a temp PEM and mock home). No root required for the default test run. @@ -347,6 +363,107 @@ sudo ./validate_certs_debian_ubuntu.sh --all-users --expected-subject "O=Example --- +## Linux (JVM): install_certs_jvm_linux.sh + +### Overview + +`install_certs_jvm_linux.sh` wires a custom CA certificate into the JVM trust path so Maven, Gradle, sbt, and Apache Ivy traffic redirected through `package-reroute` validates correctly. **JVM trust only** — does not configure Node/npm or Python, and does not touch Docker credentials. Pair with `install_certs_debian_ubuntu.sh` if you need the Node/Python flows or Docker Hub credential cleanup. + +The script auto-detects between two lab-verified paths: + +- **Path A — `update-ca-trust`** for RHEL/Fedora/CentOS/Amazon-Linux when a JDK whose `lib/security/cacerts` is symlinked to `/etc/pki/ca-trust/extracted/java/cacerts` is on `PATH` (Red Hat OpenJDK, and on some images Corretto). Drops the CA into `/etc/pki/ca-trust/source/anchors/` and runs `update-ca-trust extract`. **No env var is set.** +- **Path B — JKS + `JAVA_TOOL_OPTIONS`** for everything else (Debian/Ubuntu, manual JDK installs that don't symlink to the system store, SDKMAN, snap-confined JDKs). Builds a JKS truststore at `/etc/ssl/package-route-jvm/truststore.jks` containing only the customer CA, then sets `JAVA_TOOL_OPTIONS` in `/etc/environment`. JDK-version-agnostic by construction — one env var serves all current and future JDKs. + +Auto-detection logic (`detect_mode` in the script): + +1. If `update-ca-trust` is not on `PATH` → **Path B**. +2. If `/etc/pki/ca-trust/extracted/java/cacerts` does not exist → **Path B**. +3. If no `java` is on `PATH` → **Path A** (assumes Red Hat OpenJDK will be installed via `dnf`; the installer prints a loud end-of-run warning instructing the user to re-run with `--mode java-tool-options` if they install Corretto/Temurin/SDKMAN instead). +4. If the resolved `java`'s `lib/security/cacerts` symlinks to the RHEL system store → **Path A**; otherwise **Path B**. + +`--mode java-tool-options` or `--mode update-ca-trust` overrides the detection. + +Both scripts source a small shared file `_jvm_linux_paths.sh` for the constants block (CA basename default, anchor dir, JKS path, password, env file) so the installer and validator cannot drift. + +### Requirements + +- **Linux** (Debian/Ubuntu family OR RHEL/Fedora/CentOS/Amazon-Linux family). +- **Root** (`sudo`). +- **`openssl`** on `PATH`. +- **`keytool`** on `PATH` (provided by any JDK) **for Path B**. On Path A the verification step uses `keytool` opportunistically but the install itself does not need it; the installer emits a warning if `keytool` is missing on Path A. + +### Options + +| Option | Required | Description | +|--------|----------|-------------| +| `--use-cert ` | **Yes** | Path to an existing PEM/CRT certificate file. Validated: must be a parseable X.509, not expired, with `CA:TRUE` in basicConstraints. Bundles emit a warning (only the first cert imports). | +| `--mode auto\|java-tool-options\|update-ca-trust` | No (default: **auto**) | Override path detection. | +| `--cert-name ` | No (default: `package-route-custom-ca`) | Base name applied to the Path A anchor file (`/etc/pki/ca-trust/source/anchors/.crt`) AND the Path B JKS alias. Must match `[A-Za-z0-9._-]+`. Pass the same value to the validator. | +| `-h`, `--help` | — | Usage. | + +### Examples + +```bash +# Auto-detect; works on both RHEL family and Debian/Ubuntu +sudo ./install_certs_jvm_linux.sh --use-cert /tmp/ZscalerRoot0.pem + +# Force JAVA_TOOL_OPTIONS path even on a RHEL host +sudo ./install_certs_jvm_linux.sh --use-cert /tmp/ZscalerRoot0.pem --mode java-tool-options + +# Custom basename for the anchor file / JKS alias +sudo ./install_certs_jvm_linux.sh --use-cert /tmp/ZscalerRoot0.pem --cert-name zscaler-root +``` + +### Validation: validate_certs_jvm_linux.sh + +**`--expected-subject` is required.** The validator auto-detects which path was used (by checking for the JKS file or the anchor file), then asserts the customer CA is present by case-insensitive subject substring match. `--all-users` iterates `/home/*` and is Path B-specific (requires root; checked at `parse_args` time, fails fast). Pass `--cert-name ` if the installer was invoked with a non-default value. + +```bash +./validate_certs_jvm_linux.sh --expected-subject "O=Zscaler" +sudo ./validate_certs_jvm_linux.sh --expected-subject "O=Zscaler" --all-users +./validate_certs_jvm_linux.sh --expected-subject "O=Zscaler" --cert-name zscaler-root +``` + +Exit code 0 if all checks pass, 1 otherwise. Missing `JAVA_TOOL_OPTIONS` in a user rc file is a **warning** (not a failure) — `/etc/environment` is the authoritative source for system-wide config. Missing `keytool` while a keystore exists is a **failure** (cannot verify the core invariant). + +### Caveats + +- **`/etc/environment` activation** (Path B). GUI-launched apps (IntelliJ from the GNOME/KDE launcher) inherit `/etc/environment` via the session manager at login. Existing sessions need a logoff/login to pick up the new env var. The script updates the SUDO_USER's `.bashrc`/`.zshrc` so the *current* shell session has it without re-login. +- **Gradle Daemon caching** (Path B). A Gradle Daemon started before the env var was set still uses its captured environment. Run `gradle --stop` after onboarding. +- **`Picked up JAVA_TOOL_OPTIONS:` banner** (Path B). Every JVM prints this to stderr at startup. CI log parsers that strict-match empty-stderr need to tolerate it. +- **`changeit` truststore password.** The JKS at `/etc/ssl/package-route-jvm/truststore.jks` uses the OpenJDK convention password `changeit`. This is **not** a secret — JKS truststores protect file integrity, not contents, and the trust anchor inside is a public CA certificate. The password is persisted in `/etc/environment` via the `-Djavax.net.ssl.trustStorePassword` flag so unattended JVMs can open the store. +- **Path B truststore extends the JDK's bundled cacerts.** `-Djavax.net.ssl.trustStore=…` in OpenJDK *replaces* the JVM trust source — a JKS containing only the corporate CA would break every public-CA TLS handshake (Maven Central, Gradle plugin portal, Let's Encrypt-fronted mirrors). The installer therefore copies `$JAVA_HOME/lib/security/cacerts` to `/etc/ssl/package-route-jvm/truststore.jks` first, then `keytool -importcert` appends the corporate CA. The resulting store has ~150 public roots **plus** the corporate one. Path A is unaffected — `update-ca-trust extract` already builds the system-wide Java cacerts by merging system anchors with the JDK's defaults. +- **Mixed-distro auto-detection.** On RHEL family with a non-Red-Hat JDK on PATH (Corretto, Temurin, SDKMAN), auto-detection lands on Path B because the JDK's `lib/security/cacerts` is not symlinked to the RHEL system store. On RHEL family with **no** JDK on PATH yet, auto-detection picks Path A on the assumption that Red Hat OpenJDK will follow via `dnf` — the installer emits a loud end-of-run warning naming this assumption. +- **Snap-confined JDKs** are not configured by Path A (read-only squashfs). Auto-detection lands on Path B because the snap cacerts is not symlinked to the system one. Path B works because `JAVA_TOOL_OPTIONS` is read by the JVM at startup regardless of how the JDK was installed. +- **Container-internal JDKs.** Maven/Gradle running inside Docker on a developer machine need the CA wired into the *container* image — host-side install does not propagate. Use a `RUN` step in the Dockerfile or pass `JAVA_TOOL_OPTIONS` via `docker run -e`. +- **`MAVEN_OPTS` clobbering.** If your shell or `~/.mavenrc` sets `MAVEN_OPTS`, those args land AFTER `JAVA_TOOL_OPTIONS` and can override the trust store flags. If `mvn` fails TLS after install, check `env | grep -E '^(JAVA_TOOL_OPTIONS|MAVEN_OPTS)='` — if both are set, ensure `MAVEN_OPTS` does NOT also include `-Djavax.net.ssl.trustStore`. +- **IntelliJ per-IDE SSL store.** `~/.config/JetBrains//ssl/cacerts` is a separate trust store used by the IDE for the plugin marketplace and VCS integration — NOT by Maven/Gradle runs spawned from IntelliJ (those use the JBR's truststore which the env var path covers). If `mvn` works in Terminal but IntelliJ Maven sync fails, add the CA via Settings → Tools → Server Certificates. +- **Idempotent re-runs.** Path B re-creates the JKS each run (single alias guaranteed) and replaces (not appends) the env var line in `/etc/environment`. Path A re-copies the anchor and re-runs `update-ca-trust extract` (with fingerprint-compare to surface a deliberate replacement vs. an idempotent re-run). Running the script twice produces the same final state. +- **Existing anchor file replacement (Path A).** A file at `/etc/pki/ca-trust/source/anchors/.crt` placed by other tooling is replaced if its SHA-256 fingerprint differs from `--use-cert`. The installer prints a `[warn] Replacing existing anchor` line with both fingerprints so the swap is auditable. + +### Testing + +`./testing/test_install_certs_jvm_linux.sh` runs the Docker smoke matrix across four distro × JDK combinations in parallel from any host with Docker. Each container builds a self-signed lab CA, runs the installer, runs the validator, then exercises negative subject, idempotency, custom `--cert-name`, path-traversal rejection, malformed-PEM rejection, expired-CA rejection, and leaf-cert rejection. + +```bash +# From repo root (any host with Docker) +./testing/test_install_certs_jvm_linux.sh +``` + +Exit code 0 if all containers pass, 1 if any test fails. + +The same matrix runs on every push and pull request via `.github/workflows/ci.yml` (`test-linux-jvm` job). + +### Summary (Linux JVM) + +- **One run as root**, single cert source via `--use-cert`. +- **Path A**: anchor in `/etc/pki/ca-trust/source/anchors/.crt`; `update-ca-trust extract` updates the system Java trust store. No env var. +- **Path B**: JKS at `/etc/ssl/package-route-jvm/truststore.jks` with alias ``; `JAVA_TOOL_OPTIONS` in `/etc/environment` and in the developer user's `.bashrc`/`.zshrc`. +- **Idempotent** (both paths), **re-runnable**, **JDK-version-agnostic** across currently-supported JDKs — JKS format is still read by JDK 8–25 (Path B); Path A piggy-backs on the OS trust store that Red Hat OpenJDK already symlinks. A future JDK that drops JKS support would require a Path B format bump. +- Users must open a new login shell (or `source /etc/environment`) for env changes to take effect. `gradle --stop` to refresh the Gradle Daemon. + +--- + ## Windows: install_certs_windows.ps1 ### Overview @@ -448,5 +565,6 @@ On **push** and **pull request** to `main` or `master`, GitHub Actions runs: |-----|--------|---------| | Test (macOS) | `macos-latest` | `sudo ./testing/test_install_certs_macos.sh` | | Test (Windows) | `windows-latest` | `./testing/test_install_certs_windows.ps1` (PowerShell) | +| Test (Linux JVM) | `ubuntu-latest` | `./testing/test_install_certs_jvm_linux.sh` | There is no CI job for the Debian/Ubuntu scripts in this workflow. diff --git a/_jvm_linux_paths.sh b/_jvm_linux_paths.sh new file mode 100644 index 0000000..cb51932 --- /dev/null +++ b/_jvm_linux_paths.sh @@ -0,0 +1,55 @@ +# (c) JFrog Ltd. (2026) +# Shared constants for install_certs_jvm_linux.sh and validate_certs_jvm_linux.sh. +# Sourced — must NOT be executed directly. Has no shebang on purpose. +# +# Both scripts read this file via: +# SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# . "${SCRIPT_DIR}/_jvm_linux_paths.sh" +# +# Keep installer and validator in lockstep by changing only this file. +# +# Cross-platform siblings (keep CLI shapes and contracts in sync): +# _jvm_macos_paths.sh — per-user JKS under ~/Library +# _jvm_windows_paths.ps1 — per-user JKS under %LOCALAPPDATA% + +# Default base name for the installed CA file (Path A) and the JKS alias (Path B). +# Overridable via --cert-name on the installer; the validator must be invoked with +# the same --cert-name when a non-default value was used. +# +# CROSS-PLATFORM NOTE: Linux is the only sibling where --cert-name affects +# a filesystem-visible path (Path A: /etc/pki/ca-trust/source/anchors/${CERT_BASENAME}.crt). +# macOS and Windows treat --cert-name as a JKS-alias-only cosmetic. A fleet +# script that wraps all three installers with the same --cert-name must +# remember to also pass --cert-name to the LINUX validator. +JVM_LINUX_DEFAULT_CERT_BASENAME="package-route-custom-ca" + +# Path A — RHEL family with Red Hat OpenJDK. The JDK symlinks its +# lib/security/cacerts to RHEL_JAVA_CACERTS, so system trust IS Java trust. +RHEL_ANCHOR_DIR="/etc/pki/ca-trust/source/anchors" +RHEL_JAVA_CACERTS="/etc/pki/ca-trust/extracted/java/cacerts" + +# Path B — JKS truststore + JAVA_TOOL_OPTIONS. Used on Debian/Ubuntu, Amazon +# Corretto, Eclipse Temurin, SDKMAN-installed JDKs, and manual .tar.gz installs. +# +# Note: Linux uses /etc/ssl/package-route-jvm rather than nesting under +# /etc/ssl/JFrog/package-route-jvm (as macOS/Windows do under their per-user +# trees). The flat path matches /etc/ssl conventions; if a future JFrog tool +# needs a sibling dir, it can carve out /etc/ssl/jfrog-/ alongside. +JKS_DIR="/etc/ssl/package-route-jvm" +JKS_PATH="${JKS_DIR}/truststore.jks" + +# OpenJDK convention for cacerts and similar truststores. NOT a secret in +# this script's use case: we import only `trustedCertEntry` records (public +# CA certs), so the password protects file *integrity* via the keystore MAC +# but not any private key material. A JKS that ever holds a PrivateKeyEntry +# would additionally rely on this password to encrypt the key — not relevant +# here. Persisted in /etc/environment via -Djavax.net.ssl.trustStorePassword +# so unattended JVMs can open the store. +JKS_PASSWORD="changeit" + +ENVIRONMENT_FILE="/etc/environment" + +# 30-day expiry warn threshold (in seconds) — used by validate_pem. Must stay +# in lockstep with the macOS bash sibling (-checkend 2592000) and the Windows +# .NET sibling (AddDays(30)). Do not change without updating all three. +JVM_LINUX_EXPIRY_WARN_SECONDS=2592000 diff --git a/install_certs_jvm_linux.sh b/install_certs_jvm_linux.sh new file mode 100755 index 0000000..301f0c8 --- /dev/null +++ b/install_certs_jvm_linux.sh @@ -0,0 +1,612 @@ +#!/usr/bin/env bash +# (c) JFrog Ltd. (2026) +# Install a custom CA certificate on Linux for JVM clients (Maven, Gradle, sbt, +# Apache Ivy). Two paths depending on distro + JDK distribution: +# +# Path A — RHEL/Fedora/CentOS/Amazon-Linux + Red Hat OpenJDK: +# Drop the cert into /etc/pki/ca-trust/source/anchors/ and run +# update-ca-trust extract. Red Hat's OpenJDK symlinks its cacerts to the +# system-managed copy, so system trust IS Java trust. +# +# Path B — everything else (Debian/Ubuntu, Amazon Corretto, Eclipse Temurin, +# SDKMAN-installed JDKs, manual .tar.gz installs): +# Build a JKS truststore at /etc/ssl/package-route-jvm/truststore.jks +# containing the customer CA and set JAVA_TOOL_OPTIONS in /etc/environment +# so every JDK on the box picks it up at startup. +# +# Run: +# sudo bash install_certs_jvm_linux.sh --use-cert /path/to/cert.pem +# [--mode auto|java-tool-options|update-ca-trust] [--cert-name ] +# +# Notes: +# - Linux only (Debian/Ubuntu and RHEL/Fedora/CentOS/Amazon-Linux families). +# - Must run as root. +# - JVM trust only — does not configure npm/Python/HF and does not touch +# Docker credentials. Pair with install_certs_debian_ubuntu.sh if needed. +# - GUI-launched IDEs need a logoff/login to pick up /etc/environment. +# +# Cross-platform siblings (keep CLI shapes and contracts in sync): +# install_certs_jvm_macos.sh — LaunchAgent + per-user JKS +# install_certs_jvm_windows.ps1 — HKCU\Environment + per-user JKS +# +# Research / rationale: see the JVM client-onboarding wiki page +# https://jfrog-int.atlassian.net/wiki/spaces/RTFACT/pages/2440101931/ + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [[ ! -f "${SCRIPT_DIR}/_jvm_linux_paths.sh" ]]; then + echo "Error: _jvm_linux_paths.sh not found next to this installer (${SCRIPT_DIR})." >&2 + echo " This script reads its constants from a sibling file." >&2 + echo " Invoke as ./install_certs_jvm_linux.sh — not via 'curl | bash'" >&2 + echo " or 'sh -c \"\$(cat …)\"' (those lose BASH_SOURCE[0])." >&2 + exit 1 +fi +# shellcheck disable=SC1091 +. "${SCRIPT_DIR}/_jvm_linux_paths.sh" + +USE_CERT="" +MODE_OPT="auto" +CERT_BASENAME="${JVM_LINUX_DEFAULT_CERT_BASENAME}" + +# Tracked so install_via_jto's final summary can warn loudly if the per-user +# rc step was skipped (otherwise the [3/4] header reads like a success). +RC_UPDATED=0 + +usage() { + cat < [--mode auto|java-tool-options|update-ca-trust] [--cert-name ] + +Options: + --use-cert Path to an existing PEM/CRT certificate file (required). + --mode auto|java-tool-options|update-ca-trust Override path detection (default: auto). + auto - detect by distro + JDK trust integration + java-tool-options - force JAVA_TOOL_OPTIONS + JKS path + update-ca-trust - force RHEL system-trust path + --cert-name Base name for installed cert (default: ${CERT_BASENAME}). + Applied to the anchor file (Path A) AND the JKS alias (Path B). + -h, --help Show this help. + +Examples: + sudo $0 --use-cert /tmp/ZscalerRoot0.pem + sudo $0 --use-cert /tmp/ca.pem --mode java-tool-options + sudo $0 --use-cert /tmp/ca.pem --cert-name zscaler-root +EOF +} + +require_root() { + if [[ "$(id -u)" -ne 0 ]]; then + echo "Error: this script must be run as root." >&2 + echo "Use: sudo $0 --use-cert [--mode auto|java-tool-options|update-ca-trust]" >&2 + exit 1 + fi +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --use-cert) + USE_CERT="${2:?Error: --use-cert requires a value}" + shift 2 + ;; + --mode) + MODE_OPT="${2:?Error: --mode requires a value}" + shift 2 + ;; + --cert-name) + CERT_BASENAME="${2:?Error: --cert-name requires a value}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage >&2 + exit 1 + ;; + esac + done + + case "$MODE_OPT" in + auto|java-tool-options|update-ca-trust) ;; + *) + echo "Error: --mode must be auto, java-tool-options, or update-ca-trust (got: $MODE_OPT)." >&2 + exit 1 + ;; + esac + + if [[ -z "$USE_CERT" ]]; then + echo "Error: --use-cert is required." >&2 + usage >&2 + exit 1 + fi + + if [[ ! -f "$USE_CERT" ]]; then + echo "Error: certificate file not found: $USE_CERT" >&2 + exit 1 + fi + + if [[ -z "$CERT_BASENAME" ]]; then + echo "Error: --cert-name cannot be empty." >&2 + exit 1 + fi + + # Reject path-traversal characters so $CERT_BASENAME stays a single path segment. + if [[ ! "$CERT_BASENAME" =~ ^[A-Za-z0-9._-]+$ ]]; then + echo "Error: --cert-name must match [A-Za-z0-9._-]+ (got: $CERT_BASENAME)." >&2 + exit 1 + fi +} + +check_os() { + if [[ ! -r /etc/os-release ]]; then + echo "Error: cannot determine OS." >&2 + exit 1 + fi + + # shellcheck disable=SC1091 + . /etc/os-release + + local id="${ID:-}" id_like="${ID_LIKE:-}" + case "$id" in + ubuntu|debian|rhel|fedora|centos|rocky|almalinux|amzn) return 0 ;; + esac + case "$id_like" in + *debian*|*rhel*|*fedora*|*centos*) return 0 ;; + esac + + echo "Error: this script supports Debian/Ubuntu and RHEL/Fedora/CentOS/Amazon-Linux families." >&2 + echo "Detected ID=${id:-unknown}, ID_LIKE=${id_like:-unknown}" >&2 + exit 1 +} + +check_dependencies() { + if ! command -v openssl >/dev/null 2>&1; then + echo "Error: openssl is required but not found." >&2 + exit 1 + fi + # keytool is needed only by Path B; checked at the install_via_jto entry point. +} + +# Locate the JDK's default cacerts file. -Djavax.net.ssl.trustStore in OpenJDK +# REPLACES the JVM trust source rather than extending it — pointing JVMs at a +# JKS containing only the corporate CA breaks every public-CA TLS handshake +# (Maven Central, Gradle plugin portal, Let's Encrypt-fronted artifact mirrors). +# We copy the JDK's bundled cacerts into the target keystore first, then +# `keytool -importcert` appends our CA, so the merged store has ~150 public +# roots PLUS the corporate one. +# +# Resolution precedence: +# 1. $JAVA_HOME/lib/security/cacerts (set by every standard JDK installer) +# 2. $(dirname $(command -v keytool))/../lib/security/cacerts (works for +# stock Adoptium / Corretto / Microsoft / Zulu / RHEL OpenJDK layouts) +# 3. Hard fail with a clear message. +# +# Echoes the resolved path on stdout; exits non-zero on failure. +find_jdk_cacerts() { + local candidate="" + if [[ -n "${JAVA_HOME:-}" && -f "${JAVA_HOME}/lib/security/cacerts" ]]; then + candidate="${JAVA_HOME}/lib/security/cacerts" + else + local keytool_path keytool_dir + keytool_path="$(command -v keytool 2>/dev/null || true)" + if [[ -n "$keytool_path" ]]; then + keytool_dir="$(dirname "$(readlink -f "$keytool_path" 2>/dev/null || echo "$keytool_path")")" + if [[ -f "${keytool_dir}/../lib/security/cacerts" ]]; then + candidate="${keytool_dir}/../lib/security/cacerts" + fi + fi + fi + if [[ -z "$candidate" ]]; then + echo "Error: cannot locate the JDK's default cacerts file." >&2 + echo " Set JAVA_HOME, or ensure keytool resolves under a standard JDK bin/ layout." >&2 + echo " Tried: \$JAVA_HOME/lib/security/cacerts, \$(dirname keytool)/../lib/security/cacerts" >&2 + exit 1 + fi + echo "$candidate" +} + +validate_pem() { + local path="$1" + + # Require PEM-encoded input (text `-----BEGIN CERTIFICATE-----` block). + # Cross-platform contract: the Windows sibling auto-detects DER via + # System.Security.Cryptography.X509Certificates and silently accepts + # it, but the bash branch only handles PEM. Keep the trilogy honest by + # rejecting DER everywhere with a clear conversion hint, so the same + # cert file produces the same outcome regardless of the platform the + # operator runs the installer on. + if ! grep -q -- '-----BEGIN CERTIFICATE-----' "$path" 2>/dev/null; then + echo "Error: certificate is not PEM-encoded: $path" >&2 + echo " If it's DER, convert first:" >&2 + echo " openssl x509 -inform der -in $path -out $path.pem" >&2 + echo " then re-run with --use-cert $path.pem" >&2 + exit 1 + fi + + if ! openssl x509 -in "$path" -noout >/dev/null 2>&1; then + echo "Error: invalid PEM/CRT certificate file: $path" >&2 + exit 1 + fi + + # Reject expired anchors: keytool -importcert -noprompt accepts them silently + # and the user gets cryptic CertificateExpiredException at TLS handshake time. + if ! openssl x509 -in "$path" -checkend 0 -noout >/dev/null 2>&1; then + echo "Error: certificate has already expired: $path" >&2 + exit 1 + fi + + # Warn (don't fail) on a cert expiring within JVM_LINUX_EXPIRY_WARN_SECONDS + # (30 days). Threshold is in lockstep with the macOS/Windows siblings. + if ! openssl x509 -in "$path" -checkend "$JVM_LINUX_EXPIRY_WARN_SECONDS" -noout >/dev/null 2>&1; then + echo "[warn] certificate expires within 30 days: $path" >&2 + fi + + # Reject leaf certs: a cert without CA:TRUE in basicConstraints will import + # into a JKS truststore but PKIX path-building won't use it as a trust anchor. + local bc + bc="$(openssl x509 -in "$path" -noout -ext basicConstraints 2>/dev/null || true)" + if [[ -n "$bc" ]] && ! grep -qi 'CA:TRUE' <<<"$bc"; then + echo "Error: certificate is not a CA (basicConstraints missing CA:TRUE): $path" >&2 + echo " JKS imports succeed but PKIX rejects non-CA trust anchors." >&2 + exit 1 + fi + + # Warn on bundles: keytool -importcert -noprompt reads only the first cert, + # silently dropping intermediates. Users should split bundles or supply only the root. + local count + count="$(grep -c -- '-----BEGIN CERTIFICATE-----' "$path" 2>/dev/null || echo 0)" + if [[ "$count" -gt 1 ]]; then + echo "[warn] PEM file contains $count certificates; only the first will be imported as the JVM trust anchor." >&2 + echo " Supply only the root CA (or split the bundle) if intermediates are needed." >&2 + fi +} + +replace_export_in_file() { + local file="$1" + local var="$2" + local value="$3" + local tmp escaped + + escaped="${value//\\/\\\\}" + escaped="${escaped//\"/\\\"}" + + # Place tmp in the same directory as the target so `mv` is rename(2) (atomic) + # and a cross-filesystem copy can't half-truncate the target on disk-full. + tmp=$(mktemp -p "$(dirname "$file")") + awk -v var="$var" -v val="$escaped" ' + $0 ~ "^export " var "=" { + print "export " var "=\"" val "\"" + next + } + { print } + ' "$file" > "$tmp" + if [[ ! -s "$tmp" && -s "$file" ]]; then + rm -f "$tmp" + echo "Error: awk produced empty output; refusing to overwrite $file" >&2 + exit 1 + fi + mv "$tmp" "$file" +} + +ensure_export_in_file() { + local file="$1" + local var="$2" + local value="$3" + + touch "$file" + + if grep -qE "^export ${var}=" "$file" 2>/dev/null; then + replace_export_in_file "$file" "$var" "$value" + else + printf 'export %s="%s"\n' "$var" "$value" >> "$file" + fi +} + +get_target_user() { + local candidate + + if [[ -n "${SUDO_USER:-}" && "${SUDO_USER}" != "root" ]]; then + candidate="$SUDO_USER" + else + candidate="$(logname 2>/dev/null || true)" + if [[ -z "$candidate" || "$candidate" == "root" ]]; then + if command -v loginctl >/dev/null 2>&1; then + candidate="$(loginctl list-sessions --no-legend 2>/dev/null | awk ' + $2 >= 1000 && $3 != "root" && $4 == "seat0" { print $3; found=1; exit } + $2 >= 1000 && $3 != "root" && !fallback { fallback=$3 } + END { if (!found && fallback) print fallback } + ')" + fi + fi + fi + + if [[ -z "$candidate" || "$candidate" == "root" ]]; then + return 0 + fi + + # Reject users that don't exist in passwd (transient sessions etc.). + if ! getent passwd "$candidate" >/dev/null 2>&1; then + return 0 + fi + + echo "$candidate" +} + +get_user_home() { + local user="$1" + getent passwd "$user" | cut -d: -f6 +} + +get_user_shell() { + local user="$1" + getent passwd "$user" | cut -d: -f7 +} + +# Stdout contract: exactly one of {java-tool-options, update-ca-trust}. +# Callers MUST equality-match the returned string. Keep in sync with the +# validator's detect_mode (which adds a "none" case when no artifacts exist). +detect_mode() { + if ! command -v update-ca-trust >/dev/null 2>&1; then + echo "java-tool-options" + return + fi + if [[ ! -e "$RHEL_JAVA_CACERTS" ]]; then + echo "java-tool-options" + return + fi + + local java_bin java_home cacerts + java_bin="$(command -v java 2>/dev/null || true)" + if [[ -z "$java_bin" ]]; then + # update-ca-trust present and the RHEL system Java cacerts exists, but + # no JDK is on PATH yet. Pick the system-trust path on the assumption + # that Red Hat OpenJDK will be installed via dnf. If the user later + # installs a non-Red-Hat JDK (Corretto, Temurin, SDKMAN), they must + # re-run with --mode java-tool-options — install_via_update_ca_trust + # emits a loud end-of-run warning to that effect. + echo "update-ca-trust" + return + fi + + java_home="$(dirname "$(dirname "$(readlink -f "$java_bin")")")" + cacerts="${java_home}/lib/security/cacerts" + if [[ "$(readlink -f "$cacerts" 2>/dev/null)" == "$RHEL_JAVA_CACERTS" ]]; then + echo "update-ca-trust" + else + echo "java-tool-options" + fi +} + +install_via_update_ca_trust() { + local anchor_path="${RHEL_ANCHOR_DIR}/${CERT_BASENAME}.crt" + local java_present=0 + command -v java >/dev/null 2>&1 && java_present=1 + + echo "[1/4] Installing CA into system anchor: $anchor_path" + mkdir -p "$RHEL_ANCHOR_DIR" + + # Pre-flight: if a file already exists at the anchor path, compare + # SHA-256 fingerprints. Identical -> idempotent re-run, skip the cp. + # Different -> warn the operator that we're replacing an existing + # trust anchor so a silent overwrite (potentially clobbering a CA + # placed by other tooling under the same --cert-name basename) leaves + # a clear paper trail. + if [[ -e "$anchor_path" ]]; then + local existing_fp installer_fp + existing_fp="$(openssl x509 -in "$anchor_path" -noout -fingerprint -sha256 2>/dev/null | sed 's/.*=//' || true)" + installer_fp="$(openssl x509 -in "$USE_CERT" -noout -fingerprint -sha256 | sed 's/.*=//')" + if [[ "$existing_fp" == "$installer_fp" ]]; then + echo " Anchor already present with matching fingerprint; skipping copy." + else + echo " [warn] Replacing existing anchor at $anchor_path" >&2 + echo " existing fingerprint: $existing_fp" >&2 + echo " new fingerprint: $installer_fp" >&2 + cp "$USE_CERT" "$anchor_path" + fi + else + cp "$USE_CERT" "$anchor_path" + fi + chmod 0644 "$anchor_path" + + echo "[2/4] Running update-ca-trust extract..." + update-ca-trust extract + + echo "[3/4] Verifying Java cacerts contains the CA..." + if ! command -v keytool >/dev/null 2>&1; then + echo " [warn] keytool not on PATH; skipping Java-side verification. Install a JDK and re-run validate_certs_jvm_linux.sh." >&2 + else + local fingerprint listing + fingerprint="$(openssl x509 -in "$anchor_path" -noout -fingerprint -sha256 | sed 's/.*=//')" + + # Capture keytool's output explicitly so a real keytool failure (wrong + # password, corrupt store, missing JDK at runtime) is reported as such + # and not as a misleading "fingerprint not yet visible". + if ! listing="$(keytool -list -keystore "$RHEL_JAVA_CACERTS" -storepass "$JKS_PASSWORD" 2>&1)"; then + echo " [warn] keytool could not read $RHEL_JAVA_CACERTS. Output:" >&2 + printf '%s\n' "$listing" | head -n5 >&2 + elif grep -qiF "$fingerprint" <<<"$listing"; then + echo " OK: CA fingerprint visible in $RHEL_JAVA_CACERTS" + else + echo " [warn] CA fingerprint not yet visible in $RHEL_JAVA_CACERTS — system trust may need a fresh login session or a non-Red-Hat JDK is on PATH." >&2 + fi + fi + + echo "[4/4] Done. No env var needed; Red Hat OpenJDK reads $RHEL_JAVA_CACERTS directly." + echo + echo "Installed certificate:" + echo " $anchor_path" + echo "Java trust path:" + echo " $RHEL_JAVA_CACERTS" + + if [[ "$java_present" -eq 0 ]]; then + echo + echo "WARNING: no JDK is on PATH right now. The system-trust path was picked because" + echo " /etc/pki/ca-trust/extracted/java/cacerts exists, assuming Red Hat OpenJDK" + echo " will be installed via dnf. If you instead install Corretto, Eclipse Temurin," + echo " or any JDK whose lib/security/cacerts is NOT symlinked to the system store," + echo " this installer will NOT have configured trust for it. Re-run with" + echo " --mode java-tool-options in that case." >&2 + fi +} + +require_keytool() { + if ! command -v keytool >/dev/null 2>&1; then + echo "Error: keytool is required for --mode java-tool-options (provided by any JDK)." >&2 + echo " Debian/Ubuntu: sudo apt-get install -y default-jdk-headless" >&2 + echo " RHEL/Fedora: sudo dnf install -y java-21-openjdk-headless" >&2 + echo " Manual JDK: add \$JAVA_HOME/bin to PATH (or symlink keytool into /usr/local/bin)." >&2 + exit 1 + fi +} + +ensure_kv_in_environment_file() { + local key="$1" value="$2" tmp escaped + + touch "$ENVIRONMENT_FILE" + + escaped="${value//\\/\\\\}" + escaped="${escaped//\"/\\\"}" + + if grep -qE "^${key}=" "$ENVIRONMENT_FILE" 2>/dev/null; then + # Same-directory mktemp keeps `mv` atomic and avoids cross-fs truncation risk. + tmp=$(mktemp -p "$(dirname "$ENVIRONMENT_FILE")") + awk -v k="$key" -v v="$escaped" ' + $0 ~ "^" k "=" { print k "=\"" v "\""; next } + { print } + ' "$ENVIRONMENT_FILE" > "$tmp" + if [[ ! -s "$tmp" && -s "$ENVIRONMENT_FILE" ]]; then + rm -f "$tmp" + echo "Error: awk produced empty output; refusing to overwrite $ENVIRONMENT_FILE" >&2 + exit 1 + fi + mv "$tmp" "$ENVIRONMENT_FILE" + else + printf '%s="%s"\n' "$key" "$escaped" >> "$ENVIRONMENT_FILE" + fi + + chmod 0644 "$ENVIRONMENT_FILE" +} + +update_user_shell_rc() { + local jto_value="$1" + local target_user user_home user_shell rc_file chown_err + + target_user="$(get_target_user)" + if [[ -z "$target_user" || "$target_user" == "root" ]]; then + echo " [warn] Per-user rc not updated: could not determine non-root target user." >&2 + echo " Run with sudo as the developer user (or set SUDO_USER) so the current shell" >&2 + echo " session picks up JAVA_TOOL_OPTIONS without re-login. /etc/environment is" >&2 + echo " written either way and will reach new login sessions." >&2 + return 0 + fi + + user_home="$(get_user_home "$target_user")" + if [[ -z "$user_home" || ! -d "$user_home" ]]; then + echo " [warn] Per-user rc not updated: home not found for $target_user." >&2 + return 0 + fi + + user_shell="$(get_user_shell "$target_user")" + case "$user_shell" in + */zsh) rc_file="$user_home/.zshrc" ;; + *) rc_file="$user_home/.bashrc" ;; + esac + + touch "$rc_file" + ensure_export_in_file "$rc_file" "JAVA_TOOL_OPTIONS" "$jto_value" + if ! chown_err="$(chown "$target_user":"$target_user" "$rc_file" 2>&1)"; then + echo " [warn] chown failed on $rc_file: $chown_err" >&2 + fi + + echo " Updated $rc_file" + RC_UPDATED=1 +} + +install_via_jto() { + require_keytool + + local src_cacerts + src_cacerts="$(find_jdk_cacerts)" + echo "[1/4] Building JKS truststore at $JKS_PATH (extending $src_cacerts)..." + mkdir -p "$JKS_DIR" + chmod 0755 "$JKS_DIR" + # Copy the JDK's bundled cacerts (~150 public root CAs) as the base so the + # merged store keeps trusting Maven Central, Let's Encrypt, etc. Then + # keytool -importcert appends the corporate CA next to them. No -storetype + # flag: modern JDKs default cacerts to PKCS12 and keytool autodetects. + cp "$src_cacerts" "$JKS_PATH" + chmod 0644 "$JKS_PATH" + keytool -importcert -noprompt \ + -alias "$CERT_BASENAME" \ + -file "$USE_CERT" \ + -keystore "$JKS_PATH" \ + -storepass "$JKS_PASSWORD" >/dev/null + + # Note on inner quoting: /etc/environment uses NAME="VALUE" format and + # treats backslashes literally — there's no way to embed `"` inside the + # value without breaking the parser. The JKS_PATH and JKS_PASSWORD are + # space-free by construction (regex on --cert-name enforces no spaces; + # JKS_DIR is /etc/ssl/package-route-jvm). Adding inner quotes would + # require an alternate persistence format and is not required for the + # current spec. The Windows sibling can quote because HKCU\Environment + # is a registry REG_SZ that stores the value byte-for-byte; macOS plist + # XML has its own escaping. The bash branch deliberately stays unquoted. + local jto_value="-Djavax.net.ssl.trustStore=${JKS_PATH} -Djavax.net.ssl.trustStorePassword=${JKS_PASSWORD}" + + echo "[2/4] Writing JAVA_TOOL_OPTIONS to $ENVIRONMENT_FILE..." + ensure_kv_in_environment_file "JAVA_TOOL_OPTIONS" "$jto_value" + + echo "[3/4] Updating target user's shell rc file..." + update_user_shell_rc "$jto_value" + + echo "[4/4] Done." + echo + echo "Truststore:" + echo " $JKS_PATH (alias: $CERT_BASENAME)" + echo "JAVA_TOOL_OPTIONS:" + echo " $jto_value" + echo + echo "Notes:" + echo " - Log out and log back in for GDM/KDM-launched IDEs to pick up JAVA_TOOL_OPTIONS." + echo " - Run 'gradle --stop' to refresh the Gradle Daemon if one was already running." + echo " - The 'Picked up JAVA_TOOL_OPTIONS:' banner on stderr is expected." + + if [[ "$RC_UPDATED" -eq 0 ]]; then + echo + echo "WARNING: per-user shell rc was NOT updated; existing shells of the developer user" + echo " will not see JAVA_TOOL_OPTIONS until they log out and back in (or source" + echo " /etc/environment manually). The system-wide change in $ENVIRONMENT_FILE" + echo " takes effect on the next fresh login." >&2 + fi +} + +main() { + require_root + parse_args "$@" + check_os + check_dependencies + validate_pem "$USE_CERT" + + local mode="$MODE_OPT" + if [[ "$mode" == "auto" ]]; then + mode="$(detect_mode)" + echo "Auto-detected mode: $mode" + else + echo "Mode (forced via --mode): $mode" + fi + + case "$mode" in + update-ca-trust) install_via_update_ca_trust ;; + java-tool-options) install_via_jto ;; + *) + echo "Error: unexpected mode after detection: $mode" >&2 + exit 1 + ;; + esac +} + +main "$@" diff --git a/testing/test_install_certs_jvm_linux.sh b/testing/test_install_certs_jvm_linux.sh new file mode 100755 index 0000000..296e1b6 --- /dev/null +++ b/testing/test_install_certs_jvm_linux.sh @@ -0,0 +1,344 @@ +#!/usr/bin/env bash +# (c) JFrog Ltd. (2026) +# Docker-driven smoke test matrix for install_certs_jvm_linux.sh and +# validate_certs_jvm_linux.sh. Runs the same internal probe script across +# four distro x JDK combinations in parallel and reports per-image status. +# +# Run from the repo root: +# ./testing/test_install_certs_jvm_linux.sh +# +# Each container: +# 1. installs JDK + openssl +# 2. mints a self-signed lab CA (CA:TRUE, 7-day validity) +# 3. creates a non-root devx user (so update_user_shell_rc has a target) +# 4. runs install_certs_jvm_linux.sh + validate_certs_jvm_linux.sh +# 5. exercises 7 additional invariants: +# - subject mismatch must exit 1 +# - idempotent re-install (no duplicate lines / aliases) +# - custom --cert-name round-trips through validator +# - path-traversal --cert-name is rejected +# - malformed PEM is rejected +# - expired CA is rejected +# - leaf cert (CA:FALSE) is rejected +# +# Exit 0 iff every container reports ALL SMOKE TESTS PASSED. + +set -euo pipefail + +if ! command -v docker >/dev/null 2>&1; then + echo "Error: docker is required to run this test matrix." >&2 + exit 1 +fi + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +PROBE="$(mktemp)" +trap 'rm -f "$PROBE"' EXIT +cat > "$PROBE" <<'PROBE_EOF' +#!/usr/bin/env bash +set -euo pipefail +fail() { echo "BUG: $1" >&2; exit 1; } + +# I5: surface openssl version per-container so a future image bump that +# drops openssl below 3.2 doesn't silently turn the expired-CA assertion +# into a SKIP that nobody notices. The expired-CA case uses -not_before/ +# -not_after, which require OpenSSL 3.2+. +openssl_version="$(openssl version)" +echo "openssl: $openssl_version" +case "$openssl_version" in + OpenSSL\ [01]*|OpenSSL\ 2*|OpenSSL\ 3.0*|OpenSSL\ 3.1*) + echo " [warn] openssl < 3.2 -- expired-CA assertion will fall back to -days -1 path or SKIP" >&2 + ;; +esac + +openssl req -x509 -newkey rsa:2048 -nodes \ + -keyout /tmp/k.pem -out /tmp/ca.pem -days 7 \ + -subj "/CN=Lab JVM CA Final/O=JFrog" \ + -addext "basicConstraints=critical,CA:TRUE" 2>/dev/null + +useradd -m -s /bin/bash devx >/dev/null 2>&1 || true + +echo "=== positive: install + validate ===" +SUDO_USER=devx ./install_certs_jvm_linux.sh --use-cert /tmp/ca.pem >/dev/null +./validate_certs_jvm_linux.sh --expected-subject "Lab JVM CA Final" >/dev/null +echo " ok" + +echo "=== negative: subject mismatch must exit 1 ===" +if ./validate_certs_jvm_linux.sh --expected-subject "Microsoft Root CA NoMatch" >/dev/null 2>&1; then + fail "validator should have exited 1 on subject mismatch" +fi +echo " ok" + +echo "=== idempotency: 2nd install must produce same final state ===" +SUDO_USER=devx ./install_certs_jvm_linux.sh --use-cert /tmp/ca.pem >/dev/null +./validate_certs_jvm_linux.sh --expected-subject "Lab JVM CA Final" >/dev/null + +# I4: assert -eq 1 (not -le 1). A regression that silently DROPS the line +# from /etc/environment would slip past `-le 1` with 0; the env var should +# always be present after a successful install in Path B mode. +# On Path A the env var should NOT be set at all (0 lines expected); the +# distinction is per-distro since detect_mode picks the path. +env_lines=0 +[[ -f /etc/environment ]] && env_lines=$(grep -c '^JAVA_TOOL_OPTIONS=' /etc/environment 2>/dev/null || true) +env_lines=${env_lines:-0} +rc_lines=0 +[[ -f /home/devx/.bashrc ]] && rc_lines=$(grep -c '^export JAVA_TOOL_OPTIONS=' /home/devx/.bashrc 2>/dev/null || true) +rc_lines=${rc_lines:-0} + +# Determine which path the installer took by checking which artifact exists. +if [[ -f /etc/ssl/package-route-jvm/truststore.jks ]]; then + # Path B: JKS exists, env vars must be exactly 1 + [[ "$env_lines" -eq 1 ]] || fail "/etc/environment has $env_lines JAVA_TOOL_OPTIONS lines (expected 1 on Path B)" + [[ "$rc_lines" -eq 1 ]] || fail "/home/devx/.bashrc has $rc_lines export lines (expected 1 on Path B)" + + # Path B JKS contains the JDK's default cacerts (~150 public roots) PLUS + # exactly one corporate-CA alias. After two installs, the corporate alias + # count must be exactly 1 — the JDK aliases stay constant. + corp_alias_count="$(keytool -list -keystore /etc/ssl/package-route-jvm/truststore.jks -storepass changeit 2>/dev/null | grep -cE '^package-route-custom-ca[,[:space:]]' || true)" + corp_alias_count="${corp_alias_count:-0}" + [[ "$corp_alias_count" -eq 1 ]] || fail "JKS corporate-CA alias count after 2nd install: $corp_alias_count (expected 1)" +else + # Path A: no env vars, anchor file must exist and be exactly 1 file with our basename + [[ "$env_lines" -eq 0 ]] || fail "Path A leaked JAVA_TOOL_OPTIONS to /etc/environment ($env_lines lines)" + anchor_count="$(ls -1 /etc/pki/ca-trust/source/anchors/package-route-custom-ca.crt 2>/dev/null | wc -l | tr -d ' ')" + [[ "$anchor_count" -eq 1 ]] || fail "Path A anchor count: $anchor_count (expected 1)" +fi +echo " ok (env=$env_lines rc=$rc_lines)" + +# I1: JTO replaces-not-appends sentinel. Pre-seed a junk value in +# /etc/environment, run installer, assert the junk is gone (env var was +# REPLACED, not concatenated). Only applies when detect_mode picks Path B. +if [[ -f /etc/ssl/package-route-jvm/truststore.jks ]]; then + echo "=== JTO env var REPLACES (not appends) on re-install ===" + sed -i '/^JAVA_TOOL_OPTIONS=/d' /etc/environment + echo 'JAVA_TOOL_OPTIONS="-Dpackage-reroute-test-sentinel=must-be-replaced"' >> /etc/environment + SUDO_USER=devx ./install_certs_jvm_linux.sh --use-cert /tmp/ca.pem >/dev/null + if grep -q 'package-reroute-test-sentinel' /etc/environment; then + fail "JTO env var was APPENDED to (sentinel survived). Re-install must replace." + fi + echo " ok" +fi + +echo "=== --cert-name custom: alias and basename honor flag ===" +SUDO_USER=devx ./install_certs_jvm_linux.sh --use-cert /tmp/ca.pem --cert-name zscaler-root >/dev/null +./validate_certs_jvm_linux.sh --expected-subject "Lab JVM CA Final" --cert-name zscaler-root >/dev/null +echo " ok" + +echo "=== negative: --cert-name with bad chars must exit 1 ===" +if SUDO_USER=devx ./install_certs_jvm_linux.sh --use-cert /tmp/ca.pem --cert-name '../etc/pwned' >/dev/null 2>&1; then + fail "installer should have rejected path-traversal --cert-name" +fi +echo " ok" + +echo "=== negative: malformed PEM must exit 1 ===" +echo "not a certificate" > /tmp/bad.pem +if SUDO_USER=devx ./install_certs_jvm_linux.sh --use-cert /tmp/bad.pem >/dev/null 2>&1; then + fail "installer should have rejected malformed PEM" +fi +echo " ok" + +echo "=== negative: expired CA must exit 1 ===" +# Generate a cert with explicit past not_before / not_after when openssl supports it, +# otherwise fall back to negative -days. Verify the result is actually expired before +# running the assertion — some openssl 3.x builds treat -days -1 as 1 day in the future. +rm -f /tmp/old-ca.pem +openssl req -x509 -newkey rsa:2048 -nodes \ + -keyout /tmp/old-k.pem -out /tmp/old-ca.pem \ + -subj "/CN=Expired CA" \ + -addext "basicConstraints=critical,CA:TRUE" \ + -not_before 20200101000000Z -not_after 20200201000000Z 2>/dev/null \ +|| openssl x509 -req -in <(openssl req -new -key /tmp/k.pem -subj "/CN=Expired") \ + -signkey /tmp/k.pem -days -1 -out /tmp/old-ca.pem 2>/dev/null \ +|| true +if [[ ! -f /tmp/old-ca.pem ]] || openssl x509 -in /tmp/old-ca.pem -checkend 0 -noout >/dev/null 2>&1; then + echo " SKIP: cannot produce a verifiably expired cert with the installed openssl ($(openssl version))" +else + if SUDO_USER=devx ./install_certs_jvm_linux.sh --use-cert /tmp/old-ca.pem >/dev/null 2>&1; then + fail "installer should have rejected expired CA" + fi + echo " ok" +fi + +echo "=== negative: leaf cert (not CA) must exit 1 ===" +openssl req -x509 -newkey rsa:2048 -nodes \ + -keyout /tmp/leaf-k.pem -out /tmp/leaf.pem -days 7 \ + -subj "/CN=Leaf Not CA" \ + -addext "basicConstraints=critical,CA:FALSE" 2>/dev/null +if SUDO_USER=devx ./install_certs_jvm_linux.sh --use-cert /tmp/leaf.pem >/dev/null 2>&1; then + fail "installer should have rejected leaf cert (CA:FALSE)" +fi +echo " ok" + +echo "=== forced --mode update-ca-trust ===" +if command -v update-ca-trust >/dev/null 2>&1; then + # Reset Path B state so we can exercise Path A cleanly. + rm -rf /etc/ssl/package-route-jvm + sed -i '/^JAVA_TOOL_OPTIONS=/d' /etc/environment 2>/dev/null || true + sed -i '/^export JAVA_TOOL_OPTIONS=/d' /home/devx/.bashrc 2>/dev/null || true + + SUDO_USER=devx ./install_certs_jvm_linux.sh --use-cert /tmp/ca.pem --mode update-ca-trust >/dev/null + [[ -f /etc/pki/ca-trust/source/anchors/package-route-custom-ca.crt ]] \ + || fail "Path A anchor file not created" + keytool -list -keystore /etc/pki/ca-trust/extracted/java/cacerts -storepass changeit 2>/dev/null \ + | grep -q "$(openssl x509 -in /tmp/ca.pem -noout -fingerprint -sha256 | sed 's/.*=//')" \ + || fail "Path A: CA fingerprint not visible in system Java cacerts" + # /etc/environment must NOT have JAVA_TOOL_OPTIONS on Path A. + if [[ -f /etc/environment ]] && grep -q '^JAVA_TOOL_OPTIONS=' /etc/environment; then + fail "Path A leaked JAVA_TOOL_OPTIONS into /etc/environment" + fi + ./validate_certs_jvm_linux.sh --expected-subject "Lab JVM CA Final" >/dev/null + echo " ok" +else + echo " SKIP: update-ca-trust not installed on this distro" +fi + +echo "=== regression: dual-artifact validator must not silent-pass ===" +# Reproduce the iteration-1 fix's regression: both Path A anchor and Path B JKS +# present at once. detect_mode previously leaked warn-stdout into command +# substitution and exited 0 with NO checks run. +if command -v update-ca-trust >/dev/null 2>&1; then + # Both artifacts already exist from Path A run; force Path B side too. + SUDO_USER=devx ./install_certs_jvm_linux.sh --use-cert /tmp/ca.pem --mode java-tool-options >/dev/null + [[ -f /etc/ssl/package-route-jvm/truststore.jks ]] || fail "Path B JKS not created" + [[ -f /etc/pki/ca-trust/source/anchors/package-route-custom-ca.crt ]] || fail "Path A anchor missing" + + # Validator must NOT print 'All checks passed' silently — it must actually run + # the Path B checks (positive subject match against the JKS). + out="$(./validate_certs_jvm_linux.sh --expected-subject "Lab JVM CA Final" 2>&1)" + grep -q 'Validating Path B' <<<"$out" || fail "validator did not run Path B checks: $out" + grep -q 'All checks passed' <<<"$out" || fail "validator did not report success: $out" + echo " ok" +else + echo " SKIP: cannot construct dual-artifact state without update-ca-trust" +fi + +echo "=== validate_pem 30-day-expiry warn fires (1-day cert) ===" +# I2 backport from macOS/Windows: assert the warn is emitted but install succeeds. +openssl req -x509 -newkey rsa:2048 -nodes \ + -keyout /tmp/soon-k.pem -out /tmp/soon-ca.pem -days 1 \ + -subj "/CN=Soon to Expire/O=JFrog" \ + -addext "basicConstraints=critical,CA:TRUE" 2>/dev/null +warn_out="$(SUDO_USER=devx ./install_certs_jvm_linux.sh --use-cert /tmp/soon-ca.pem 2>&1)" +if ! echo "$warn_out" | grep -q "certificate expires within 30 days"; then + echo "$warn_out" | tail -10 + fail "30-day expiry warn not emitted" +fi +echo " ok" + +echo "=== validate_pem multi-cert bundle warn fires ===" +# I2 backport: concat two certs, assert the warn is emitted but install succeeds. +cat /tmp/ca.pem /tmp/soon-ca.pem > /tmp/bundle.pem +warn_out="$(SUDO_USER=devx ./install_certs_jvm_linux.sh --use-cert /tmp/bundle.pem 2>&1)" +if ! echo "$warn_out" | grep -qE "PEM file contains [0-9]+ certificates"; then + echo "$warn_out" | tail -10 + fail "multi-cert bundle warn not emitted" +fi +echo " ok" + +echo "=== negative: missing keytool fails cleanly (Path B) ===" +# I3 backport from Windows: temporarily hide every keytool on PATH (incl. the +# JDK-installed one symlinked via update-alternatives), assert the installer +# exits non-zero with a message mentioning keytool. Path A is keytool-optional +# so we only exercise Path B (--mode java-tool-options). +keytool_paths=() +while IFS= read -r p; do keytool_paths+=("$p"); done < <(command -v keytool 2>/dev/null; type -ap keytool 2>/dev/null | sort -u) +for p in "${keytool_paths[@]}"; do mv "$p" "$p.hidden_for_test" 2>/dev/null || true; done +trap 'for p in "${keytool_paths[@]}"; do [[ -f "$p.hidden_for_test" ]] && mv "$p.hidden_for_test" "$p" 2>/dev/null || true; done' EXIT +if SUDO_USER=devx ./install_certs_jvm_linux.sh --use-cert /tmp/ca.pem --mode java-tool-options >/tmp/nokey.out 2>&1; then + cat /tmp/nokey.out | head -10 + fail "installer should have rejected missing keytool" +fi +if ! grep -q -i keytool /tmp/nokey.out; then + cat /tmp/nokey.out | head -10 + fail "missing-keytool error message should mention 'keytool'" +fi +# Restore now (and clear the EXIT trap) so subsequent tests can use keytool. +for p in "${keytool_paths[@]}"; do [[ -f "$p.hidden_for_test" ]] && mv "$p.hidden_for_test" "$p" 2>/dev/null || true; done +trap - EXIT +echo " ok" + +echo "=== negative: DER cert rejected (C1 cross-platform parity) ===" +# C1 backport: convert the lab CA to DER, then attempt install — should +# fail with a hint to convert back. Mirrors Windows behavior (which now +# also rejects DER for cross-platform symmetry). +openssl x509 -in /tmp/ca.pem -outform DER -out /tmp/ca.der 2>/dev/null +if SUDO_USER=devx ./install_certs_jvm_linux.sh --use-cert /tmp/ca.der >/dev/null 2>&1; then + fail "installer should have rejected DER-encoded cert" +fi +echo " ok" + +echo "=== Path B: JKS extends default cacerts (preserves public roots) ===" +# Bug 2 regression guard. -Djavax.net.ssl.trustStore in OpenJDK REPLACES the +# JVM trust source (it does not merge with $JAVA_HOME/lib/security/cacerts). +# We must therefore ship a JKS that already contains the JDK's ~150 public +# roots PLUS the corporate CA. A future change that re-shrinks the truststore +# to just the corporate CA would break every public-CA TLS handshake. +if [[ -f /etc/ssl/package-route-jvm/truststore.jks ]]; then + # Re-build via Path B so we measure the post-fix state. + SUDO_USER=devx ./install_certs_jvm_linux.sh --use-cert /tmp/ca.pem --mode java-tool-options >/dev/null + alias_count=$(keytool -list -keystore /etc/ssl/package-route-jvm/truststore.jks \ + -storepass changeit 2>/dev/null | grep -c 'trustedCertEntry' || true) + alias_count=${alias_count:-0} + [[ "$alias_count" -ge 100 ]] || fail "Path B truststore has $alias_count aliases; expected >= 100 (JDK cacerts ~150 public roots + corporate CA)" + keytool -list -keystore /etc/ssl/package-route-jvm/truststore.jks -storepass changeit 2>/dev/null \ + | grep -qi 'digicert' \ + || fail "Path B truststore is missing the DigiCert family of public roots; the copy-from-JDK step did not run" + echo " ok ($alias_count aliases, DigiCert present)" +else + echo " SKIP: Path B not exercised on this distro (Path A only)" +fi + +echo "=== ALL SMOKE TESTS PASSED ===" +PROBE_EOF +chmod +x "$PROBE" + +# distro_id|image|setup_command +MATRIX=( + "ubuntu|ubuntu:22.04|export DEBIAN_FRONTEND=noninteractive; apt-get update -qq >/dev/null && apt-get install -y -qq --no-install-recommends openssl ca-certificates default-jdk-headless >/dev/null" + "debian|debian:12|export DEBIAN_FRONTEND=noninteractive; apt-get update -qq >/dev/null && apt-get install -y -qq --no-install-recommends openssl ca-certificates default-jdk-headless >/dev/null" + "rhel|redhat/ubi9:latest|dnf install -y -q java-21-openjdk-headless openssl shadow-utils >/dev/null" + "amazonlinux|amazonlinux:2023|dnf install -y -q java-21-amazon-corretto-headless openssl shadow-utils >/dev/null" +) + +LOG_DIR="$(mktemp -d)" +trap 'rm -f "$PROBE"; rm -rf "$LOG_DIR"' EXIT + +pids=() +labels=() + +for entry in "${MATRIX[@]}"; do + distro="${entry%%|*}" + rest="${entry#*|}" + image="${rest%%|*}" + setup="${rest#*|}" + + log="${LOG_DIR}/${distro}.log" + ( + docker run --rm \ + -v "${REPO_ROOT}":/lab \ + -v "${PROBE}":/probe.sh:ro \ + -w /lab "$image" bash -c "set -e; ${setup}; bash /probe.sh" + ) >"$log" 2>&1 & + pids+=("$!") + labels+=("$distro ($image)") + echo "[launched] $distro ($image) -> $log" +done + +overall_rc=0 +for i in "${!pids[@]}"; do + if wait "${pids[$i]}"; then + echo "[PASS] ${labels[$i]}" + else + echo "[FAIL] ${labels[$i]} — last 30 lines:" + tail -30 "${LOG_DIR}/${labels[$i]%% *}.log" | sed 's/^/ /' + overall_rc=1 + fi +done + +if [[ "$overall_rc" -eq 0 ]]; then + echo "All distros passed." +else + echo "One or more distros failed." +fi + +exit "$overall_rc" diff --git a/validate_certs_jvm_linux.sh b/validate_certs_jvm_linux.sh new file mode 100755 index 0000000..cc5d22c --- /dev/null +++ b/validate_certs_jvm_linux.sh @@ -0,0 +1,333 @@ +#!/usr/bin/env bash +# (c) JFrog Ltd. (2026) +# Validate JVM truststore installation done by install_certs_jvm_linux.sh. +# +# Detects which path the installer used (RHEL update-ca-trust vs JKS + +# JAVA_TOOL_OPTIONS), then asserts: +# 1. Anchor file (Path A) OR JKS truststore (Path B) exists +# 2. Customer CA subject matches --expected-subject (substring, case-insensitive) +# 3. (Path B) /etc/environment contains JAVA_TOOL_OPTIONS pointing at the JKS +# 4. (Path B) Current user's shell rc files reference the same value (WARN if missing) +# 5. Current process inherited JAVA_TOOL_OPTIONS (HINT if missing — open a new shell) +# +# Run: +# bash validate_certs_jvm_linux.sh --expected-subject "O=Zscaler" +# sudo bash validate_certs_jvm_linux.sh --expected-subject "O=Zscaler" --all-users +# +# Pass the same --cert-name that was used during install if it was non-default. +# +# Exit 0 = all checks passed (warnings tolerated). +# +# Cross-platform siblings (keep CLI shapes and contracts in sync): +# validate_certs_jvm_macos.sh — LaunchAgent + launchctl getenv check +# validate_certs_jvm_windows.ps1 — HKCU\Environment + Get-JavaToolOptions check +# +# Research / rationale: see the JVM client-onboarding wiki page +# https://jfrog-int.atlassian.net/wiki/spaces/RTFACT/pages/2440101931/ + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [[ ! -f "${SCRIPT_DIR}/_jvm_linux_paths.sh" ]]; then + echo "Error: _jvm_linux_paths.sh not found next to this validator (${SCRIPT_DIR})." >&2 + echo " Invoke as ./validate_certs_jvm_linux.sh — not via 'curl | bash'." >&2 + exit 1 +fi +# shellcheck disable=SC1091 +. "${SCRIPT_DIR}/_jvm_linux_paths.sh" + +ALL_USERS=0 +EXPECTED_SUBJECT="" +CERT_BASENAME="${JVM_LINUX_DEFAULT_CERT_BASENAME}" + +usage() { + cat < [--cert-name ] [--all-users] + +Options: + --expected-subject Required. Case-insensitive substring match against the cert subject. + --cert-name Base name used at install (default: ${CERT_BASENAME}). + Must match the installer's --cert-name when non-default. + --all-users For Path B (JKS+JTO), validate /home/* users' rc files (requires root). + -h, --help Show this help + +Exits 0 if all checks pass, 1 if any check fails. +EOF +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --all-users) + ALL_USERS=1 + shift + ;; + --expected-subject) + EXPECTED_SUBJECT="${2:?Error: --expected-subject requires a value}" + shift 2 + ;; + --cert-name) + CERT_BASENAME="${2:?Error: --cert-name requires a value}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage >&2 + exit 1 + ;; + esac + done + + if [[ -z "$EXPECTED_SUBJECT" ]]; then + echo "Error: --expected-subject is required." >&2 + usage >&2 + exit 1 + fi + + if [[ -z "$CERT_BASENAME" ]]; then + echo "Error: --cert-name cannot be empty." >&2 + exit 1 + fi + + # Symmetrical with installer: same regex constraint so the file path resolves cleanly. + if [[ ! "$CERT_BASENAME" =~ ^[A-Za-z0-9._-]+$ ]]; then + echo "Error: --cert-name must match [A-Za-z0-9._-]+ (got: $CERT_BASENAME)." >&2 + exit 1 + fi + + if [[ "$ALL_USERS" -eq 1 && "$(id -u)" -ne 0 ]]; then + echo "Error: --all-users requires root. Use: sudo $0 --all-users --expected-subject ..." >&2 + exit 1 + fi +} + +FAIL=0 +fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); } +ok() { echo " OK: $1"; } +warn() { echo " WARN: $1"; } +skip() { echo " SKIP: $1"; } + +# Stdout contract: exactly one of {java-tool-options, update-ca-trust, none}. +# Callers MUST equality-match. Keep in sync with installer's detect_mode. +detect_mode() { + local anchor="${RHEL_ANCHOR_DIR}/${CERT_BASENAME}.crt" + local jks_present=0 anchor_present=0 + [[ -f "$JKS_PATH" ]] && jks_present=1 + [[ -f "$anchor" ]] && anchor_present=1 + + if [[ "$jks_present" -eq 1 && "$anchor_present" -eq 1 ]]; then + # detect_mode is consumed via $(...), so anything on stdout that is + # NOT the mode token leaks into the caller's $mode and breaks the case + # dispatch. Route the whole warn block to stderr. + { + warn "Both Path A and Path B artifacts exist on disk:" + warn " $JKS_PATH" + warn " $anchor" + warn "Reporting Path B (more recent mode preferred). Clean up the unused path manually." + } >&2 + echo "java-tool-options" + return + fi + + if [[ "$jks_present" -eq 1 ]]; then + echo "java-tool-options" + return + fi + if [[ "$anchor_present" -eq 1 ]]; then + echo "update-ca-trust" + return + fi + echo "none" +} + +validate_pem_subject() { + local path="$1" + if [[ ! -f "$path" ]]; then + fail "file does not exist: $path" + return 1 + fi + if ! openssl x509 -in "$path" -noout >/dev/null 2>&1; then + fail "not a valid PEM certificate: $path" + return 1 + fi + local subject + subject="$(openssl x509 -in "$path" -noout -subject 2>/dev/null)" + if echo "$subject" | grep -qi "$EXPECTED_SUBJECT"; then + ok "PEM at $path has subject matching: $EXPECTED_SUBJECT" + return 0 + fi + fail "PEM at $path subject does not match '$EXPECTED_SUBJECT' (got: $subject)" + return 1 +} + +validate_keystore_contains_subject() { + local keystore="$1" storepass="$2" label="$3" + if [[ ! -f "$keystore" ]]; then + fail "$label keystore does not exist: $keystore" + return 1 + fi + if ! command -v keytool >/dev/null 2>&1; then + # Promoting to FAIL: the keystore-subject check is the validator's + # core assertion; silently passing here would mean CI is green even + # though we never verified the cert is in the store. + fail "$label keystore present but keytool is not on PATH; cannot verify subject. Install a JDK." + return 1 + fi + + # Capture keytool output explicitly so a real keytool error (wrong password, + # corrupt store, version mismatch) is reported as such — and not silently + # masked as a "subject not found" via pipefail+|| true on the pipeline. + local keytool_output rc=0 + if ! keytool_output="$(keytool -list -v -keystore "$keystore" -storepass "$storepass" 2>&1)"; then + fail "$label: keytool could not read the keystore. Output (first 3 lines):" + printf '%s\n' "$keytool_output" | head -n3 | sed 's/^/ /' + return 1 + fi + + # Two-stage filter via a variable, not a pipe-pair: under `set -o pipefail` + # the second `grep -qi` exits early on match and SIGPIPEs the first, which + # poisons the pipeline status and turns positive matches into false negatives. + local owners + owners="$(printf '%s\n' "$keytool_output" | grep -E '^Owner:' || true)" + if ! grep -qi "$EXPECTED_SUBJECT" <<<"$owners"; then + fail "$label has no cert with subject matching: $EXPECTED_SUBJECT" + return 1 + fi + + # I8: defence-in-depth — assert every alias is a `trustedCertEntry`. + # The installer only calls `keytool -importcert`, which creates + # trustedCertEntry records. A `PrivateKeyEntry` showing up here would mean + # someone (or a compromised future installer change) imported a keypair + # into this store — the password protecting that key would then be + # `changeit`, which is the well-known JDK default and unsuitable for + # private-key material. Refuse to validate such a store. + if printf '%s\n' "$keytool_output" | grep -qE '^Entry type: PrivateKeyEntry'; then + fail "$label contains a PrivateKeyEntry — this truststore must hold only trustedCertEntry records." + return 1 + fi + ok "$label contains cert with subject matching: $EXPECTED_SUBJECT" + return 0 +} + +validate_export_in_file() { + local file="$1" label="$2" + if [[ ! -f "$file" ]]; then + skip "$label not present: $file" + return 0 + fi + if [[ ! -r "$file" ]]; then + warn "$label not readable by current user; re-run as that user or with sudo --all-users." + return 0 + fi + # Accept either quoted or unquoted trustStore=. Installer now writes + # the quoted form (cross-platform symmetry with macOS/Windows); older + # installs may carry the unquoted form. Both are valid JTO values. + if grep -qE "^export JAVA_TOOL_OPTIONS=.*trustStore=\"?${JKS_PATH}\"?" "$file" 2>/dev/null; then + ok "$label contains JAVA_TOOL_OPTIONS pointing at $JKS_PATH" + return 0 + fi + if grep -qE '^export JAVA_TOOL_OPTIONS=' "$file" 2>/dev/null; then + # Wrong-target export is a real problem: it overrides /etc/environment. + fail "$label has JAVA_TOOL_OPTIONS but it does not point at $JKS_PATH" + return 1 + fi + # Missing export in user rc is not authoritative — /etc/environment is the source of truth. + warn "$label has no JAVA_TOOL_OPTIONS export (current session may need re-login or 'source $file')" + return 0 +} + +validate_environment_file() { + if [[ ! -f "$ENVIRONMENT_FILE" ]]; then + fail "$ENVIRONMENT_FILE does not exist" + return 1 + fi + # Accept either quoted or unquoted trustStore=; see validate_export_in_file. + if grep -qE "^JAVA_TOOL_OPTIONS=.*trustStore=\"?${JKS_PATH}\"?" "$ENVIRONMENT_FILE" 2>/dev/null; then + ok "$ENVIRONMENT_FILE contains JAVA_TOOL_OPTIONS pointing at $JKS_PATH" + return 0 + fi + fail "$ENVIRONMENT_FILE has no JAVA_TOOL_OPTIONS pointing at $JKS_PATH" + return 1 +} + +validate_path_a() { + echo "Validating Path A (RHEL update-ca-trust)..." + local anchor="${RHEL_ANCHOR_DIR}/${CERT_BASENAME}.crt" + validate_pem_subject "$anchor" || true + validate_keystore_contains_subject "$RHEL_JAVA_CACERTS" "$JKS_PASSWORD" "Java cacerts" || true +} + +validate_path_b() { + echo "Validating Path B (JKS + JAVA_TOOL_OPTIONS)..." + validate_keystore_contains_subject "$JKS_PATH" "$JKS_PASSWORD" "Truststore $JKS_PATH" || true + validate_environment_file || true + + if [[ "$ALL_USERS" -eq 1 ]]; then + local homedir user + for homedir in /home/*; do + [[ -d "$homedir" ]] || continue + user="$(basename "$homedir")" + echo " Checking user $user ..." + validate_export_in_file "$homedir/.bashrc" "$user .bashrc" || true + validate_export_in_file "$homedir/.zshrc" "$user .zshrc" || true + done + else + echo " Checking current user ..." + [[ -z "${HOME:-}" ]] && HOME=$(eval echo "~") + validate_export_in_file "$HOME/.bashrc" "current user .bashrc" || true + validate_export_in_file "$HOME/.zshrc" "current user .zshrc" || true + fi + + # Current-process env hint: the most common "validator passed but + # Maven still fails TLS" support ticket. /etc/environment is the + # authoritative source for new login sessions, but the SHELL THIS + # VALIDATOR IS RUNNING IN inherits its env at startup. If the user + # ran install -> validate in the same shell, the env var won't be set. + if [[ -z "${JAVA_TOOL_OPTIONS:-}" ]]; then + echo " HINT: JAVA_TOOL_OPTIONS is NOT set in this shell. Open a new" + echo " login shell, or 'source $ENVIRONMENT_FILE', then re-run" + echo " the build. The next 'mvn'/'gradle' won't see the trust" + echo " store until then." + fi +} + +main() { + parse_args "$@" + + echo "Expected subject (case-insensitive substring): $EXPECTED_SUBJECT" + echo + + local mode + mode="$(detect_mode)" + echo "Detected installer path: $mode" + echo + + case "$mode" in + update-ca-trust) validate_path_a ;; + java-tool-options) validate_path_b ;; + none) + echo "Error: no install_certs_jvm_linux.sh artifacts found for cert-name '$CERT_BASENAME'." >&2 + echo " Expected one of:" >&2 + echo " $JKS_PATH (Path B)" >&2 + echo " ${RHEL_ANCHOR_DIR}/${CERT_BASENAME}.crt (Path A)" >&2 + echo " If you installed with a non-default --cert-name, pass --cert-name here too." >&2 + exit 1 + ;; + esac + + echo "---------------------------------------------------" + if [[ "$FAIL" -eq 0 ]]; then + echo "Result: All checks passed." + exit 0 + else + echo "Result: $FAIL check(s) failed." + exit 1 + fi +} + +main "$@"