Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,29 @@ jobs:
- name: Run macOS tests
run: sudo ./testing/test_install_certs_macos.sh

test-macos-jvm:
name: Test (macOS JVM)
runs-on: macos-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'

# Pass JAVA_HOME and PATH explicitly to the sudo'd test runner rather
# than relying on `sudo --preserve-env=…`: future actions/setup-java
# versions could rename or rescope the JAVA_HOME env var, silently
# breaking the preserve-by-name approach. Explicit `env VAR=…` is
# the same idiom and survives such churn.
- name: Run macOS JVM smoke matrix
run: |
sudo env \
JAVA_HOME="$JAVA_HOME" \
PATH="$JAVA_HOME/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" \
./testing/test_install_certs_jvm_macos.sh

test-windows:
name: Test (Windows)
runs-on: windows-latest
Expand All @@ -27,3 +50,32 @@ jobs:
- name: Run Windows tests
shell: pwsh
run: ./testing/test_install_certs_windows.ps1

test-windows-jvm:
name: Test (Windows JVM)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'

- name: Run Windows JVM smoke matrix
shell: pwsh
run: ./testing/test_install_certs_jvm_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
350 changes: 349 additions & 1 deletion README.md

Large diffs are not rendered by default.

222 changes: 222 additions & 0 deletions build_jvm_truststore_linux.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
#!/usr/bin/env bash
# (c) JFrog Ltd. (2026)
# Build a JVM truststore from the Linux system CA bundle plus one custom CA PEM.
#
# This is a build-time helper for the JVM installers. It does not install
# anything and does not require root.
#
# Run:
# ./build_jvm_truststore_linux.sh --use-cert /path/to/company-ca.pem --output /tmp/package-route-truststore.jks

set -euo pipefail

JKS_PASSWORD="changeit"
DEFAULT_CERT_ALIAS="package-route-custom-ca"

USE_CERT=""
OUTPUT=""
CERT_ALIAS="$DEFAULT_CERT_ALIAS"
SYSTEM_BUNDLE=""
OPENSSL_BIN="${OPENSSL:-openssl}"

usage() {
cat <<EOF
Usage:
$0 --use-cert <path> --output <path> [--cert-alias <alias>] [--system-bundle <path>]

Options:
--use-cert <path> PEM certificate to add to the truststore. Must contain
exactly one non-expired CA certificate with CA:TRUE.
--output <path> Destination truststore path. Replaced atomically after
successful build.
--cert-alias <alias> Alias for the custom CA (default: ${DEFAULT_CERT_ALIAS}).
--system-bundle <path> Override the detected Linux system CA PEM bundle.
-h, --help Show this help.

The generated truststore uses password '${JKS_PASSWORD}'.
EOF
}

parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--use-cert)
USE_CERT="${2:?Error: --use-cert requires a value}"
shift 2
;;
--output)
OUTPUT="${2:?Error: --output requires a value}"
shift 2
;;
--cert-alias)
CERT_ALIAS="${2:?Error: --cert-alias requires a value}"
shift 2
;;
--system-bundle)
SYSTEM_BUNDLE="${2:?Error: --system-bundle requires a value}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
esac
done

[[ -n "$USE_CERT" ]] || { echo "Error: --use-cert is required." >&2; usage >&2; exit 1; }
[[ -n "$OUTPUT" ]] || { echo "Error: --output is required." >&2; usage >&2; exit 1; }
[[ "$CERT_ALIAS" =~ ^[A-Za-z0-9._-]+$ ]] || {
echo "Error: --cert-alias must match [A-Za-z0-9._-]+ (got: $CERT_ALIAS)." >&2
exit 1
}
}

check_dependencies() {
command -v keytool >/dev/null 2>&1 || { echo "Error: keytool is required." >&2; exit 1; }
command -v "$OPENSSL_BIN" >/dev/null 2>&1 || { echo "Error: openssl is required." >&2; exit 1; }
}

detect_system_bundle() {
if [[ -n "$SYSTEM_BUNDLE" ]]; then
[[ -f "$SYSTEM_BUNDLE" && -r "$SYSTEM_BUNDLE" && -s "$SYSTEM_BUNDLE" ]] || {
echo "Error: --system-bundle must point to a readable non-empty file: $SYSTEM_BUNDLE" >&2
exit 1
}
echo "$SYSTEM_BUNDLE"
return 0
fi

local candidate
for candidate in \
/etc/ssl/certs/ca-certificates.crt \
/etc/pki/tls/certs/ca-bundle.crt \
/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem \
/etc/ssl/ca-bundle.pem; do
if [[ -f "$candidate" && -r "$candidate" && -s "$candidate" ]]; then
echo "$candidate"
return 0
fi
done

echo "Error: could not find a Linux system CA bundle. Pass --system-bundle <path>." >&2
exit 1
}

validate_custom_pem() {
local path="$1" count bc

[[ -f "$path" && -r "$path" && -s "$path" ]] || {
echo "Error: --use-cert must point to a readable non-empty file: $path" >&2
exit 1
}

count="$(grep -c -- '-----BEGIN CERTIFICATE-----' "$path" 2>/dev/null || true)"
if [[ "$count" -ne 1 ]]; then
echo "Error: --use-cert must contain exactly one PEM certificate (found $count): $path" >&2
exit 1
fi
"$OPENSSL_BIN" x509 -in "$path" -noout >/dev/null 2>&1 || {
echo "Error: invalid PEM certificate: $path" >&2
exit 1
}
"$OPENSSL_BIN" x509 -in "$path" -checkend 0 -noout >/dev/null 2>&1 || {
echo "Error: certificate has already expired: $path" >&2
exit 1
}
bc="$("$OPENSSL_BIN" x509 -in "$path" -noout -ext basicConstraints 2>/dev/null || true)"
if ! grep -qi 'CA:TRUE' <<<"$bc"; then
echo "Error: certificate is not a CA (basicConstraints missing CA:TRUE): $path" >&2
exit 1
fi
}

split_pem_bundle() {
local bundle="$1" out_dir="$2"
awk -v dir="$out_dir" '
/-----BEGIN CERTIFICATE-----/ { n++; file=sprintf("%s/cert-%05d.pem", dir, n) }
file != "" { print > file }
/-----END CERTIFICATE-----/ { file="" }
' "$bundle"
}

cert_fingerprint() {
"$OPENSSL_BIN" x509 -in "$1" -noout -fingerprint -sha256 \
| sed 's/.*=//' | tr -d ':' | tr '[:upper:]' '[:lower:]'
}

import_cert() {
local cert="$1" alias="$2" truststore="$3" keytool_out

if ! keytool_out="$(keytool -importcert -noprompt -storetype JKS \
-alias "$alias" \
-file "$cert" \
-keystore "$truststore" \
-storepass "$JKS_PASSWORD" 2>&1)"; then
echo "Error: keytool failed while importing $cert as $alias. Output:" >&2
printf '%s\n' "$keytool_out" | sed 's/^/ /' >&2
exit 1
fi
}

build_truststore() {
local system_bundle="$1" tmpdir cert fp imported_count=0 tmp_store seen

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
mkdir -p "$tmpdir/system"
seen="$tmpdir/seen-fingerprints.txt"
: > "$seen"
tmp_store="$tmpdir/truststore.jks"

split_pem_bundle "$system_bundle" "$tmpdir/system"
for cert in "$tmpdir"/system/*.pem; do
[[ -s "$cert" ]] || continue
if ! "$OPENSSL_BIN" x509 -in "$cert" -noout >/dev/null 2>&1; then
continue
fi
fp="$(cert_fingerprint "$cert")"
if grep -qx "$fp" "$seen"; then
continue
fi
printf '%s\n' "$fp" >> "$seen"
import_cert "$cert" "system-$fp" "$tmp_store"
imported_count=$((imported_count + 1))
done

if [[ "$imported_count" -eq 0 ]]; then
echo "Error: no certificates could be imported from system bundle: $system_bundle" >&2
exit 1
fi

import_cert "$USE_CERT" "$CERT_ALIAS" "$tmp_store"

mkdir -p "$(dirname "$OUTPUT")"
mv "$tmp_store" "$OUTPUT"
chmod 0644 "$OUTPUT"

echo "Built JVM truststore:"
echo " $OUTPUT"
echo "System bundle:"
echo " $system_bundle"
echo "Imported system certificates:"
echo " $imported_count"
echo "Custom CA alias:"
echo " $CERT_ALIAS"
}

main() {
parse_args "$@"
check_dependencies
validate_custom_pem "$USE_CERT"

local system_bundle
system_bundle="$(detect_system_bundle)"
build_truststore "$system_bundle"
}

main "$@"
Loading
Loading