diff --git a/federation/README.md b/federation/README.md index 5eadafd..66130f3 100644 --- a/federation/README.md +++ b/federation/README.md @@ -101,6 +101,67 @@ For example: `docker network connect openfire-testing_openfire-federated-net openfire-testing_xmpp1_1` +## Certificates + +By default, the system uses self-signed certificates that have been pre-generated and added +to the identity and trust stores for `xmpp1.localhost.example` and `xmpp2.localhost.example`. + +This simplifies the setup process and avoids the need to generate certificates, but the +limitations of this are that the certificates are not trusted by anything other than +our own servers (`xmpp1` and `xmpp2`), and that they do not support OCSP (Online Certificate +Status Protocol). + +### OCSP Support +A more fully featured solution is provided by the `-o` option of the `start.sh` script +which enables OCSP support for the Openfire federated environment. When passed the `-o` +option the script will generate a complete certificate hierarchy with OCSP support, and +deploy an OCSP responder service configured to respond to OCSP requests for the server +certificates. + +Here's what the script creates: + +* Root CA certificate (self-signed) +* Intermediate CA certificate (signed by Root CA) +* Two server certificates with OCSP information (one for each Openfire instance) +* An OCSP responder certificate (for signing OCSP responses) +* Full certificate chains for both servers (server + intermediate + root) +* Certificate database (index.txt) for the OCSP responder to track certificate statuses + +All certificates are stored in `./_data/certs/`. + +``` + Root CA + (Top level trust root) + (Kept offline/secure) + | + v + Intermediate CA + (Day-to-day certificate issuer) + (OCSP configuration point) + | + +--------------------+------------------+ + v v v + XMPP1 Cert XMPP2 Cert OCSP Cert + | | (Signs OCSP responses) + v v + XMPP1 Server XMPP2 Server + [keystore] [keystore] + (server's identity) (server's identity) + [truststore] [truststore] + (who to trust) (who to trust) +``` + +This setup allows certificates to be checked for revocation status making a request to the +OCSP responder: +```bash +openssl ocsp -url http://localhost:8888 \ + -issuer _data/certs/ca/intermediate-ca/intermediate.crt \ + -CAfile _data/certs/chain1.pem \ + -cert _data/certs/server1.crt \ + -text +``` + + ## How it's built To recreate the known good state for the system we first create base Openfire and Postgres containers. diff --git a/federation/docker-compose-ocsp-responder.yml b/federation/docker-compose-ocsp-responder.yml new file mode 100644 index 0000000..e9c428f --- /dev/null +++ b/federation/docker-compose-ocsp-responder.yml @@ -0,0 +1,42 @@ +services: + # OCSP (Online Certificate Status Protocol) Responder Service + # + # This service provides real-time certificate validation for the development environment. + # It works with certificates generated by ./scripts/generate-certificates.sh and imported + # by ./scripts/import-certificates.sh. + # + # Configuration Parameters: + # ------------------------ + # Port: 8888 - OCSP responder listens for validation requests + # Index File: - Lists all issued certificates (/ca/intermediate-ca/index.txt) + # CA Certificate: - Issuer's certificate (/ca/intermediate-ca/intermediate.crt) + # OCSP Key Pair: - Responder credentials (/ca/ocsp-responder/ocsp.{key,crt}) + # Validity Period: - Responses valid for 1 day (-ndays 1) + # + # Test Certificate Status: + # ---------------------- + # openssl ocsp -url http://localhost:8888 \ + # -issuer _data/certs/ca/intermediate-ca/intermediate.crt \ + # -CAfile _data/certs/chain1.pem \ + # -cert _data/certs/server1.crt \ + # -text # Adds human-readable output + ocsp-responder: + image: alpine:latest + volumes: + - ./_data/certs/ca:/ca + command: + - /bin/sh + - -c + - | + apk add --no-cache openssl && + openssl ocsp -port 8888 -text \ + -index /ca/intermediate-ca/index.txt \ + -CA /ca/intermediate-ca/intermediate.crt \ + -rkey /ca/ocsp-responder/ocsp.key \ + -rsigner /ca/ocsp-responder/ocsp.crt \ + -ndays 1 + ports: + - "8888:8888" + networks: + openfire-federated-net: + ipv4_address: 172.50.0.30 \ No newline at end of file diff --git a/federation/scripts/generate-certificates.sh b/federation/scripts/generate-certificates.sh new file mode 100755 index 0000000..7df61fe --- /dev/null +++ b/federation/scripts/generate-certificates.sh @@ -0,0 +1,160 @@ +#!/bin/bash + +# OCSP server configuration +# Defines where the OCSP responder will be accessible in the Docker network +OCSP_URL="http://172.50.0.30:8888" + +# Base directory for all certificate-related files +# All paths in this script will be relative to this directory +CERT_DIR="./_data/certs" + +# Create the PKI directory structure: +# We only need: +# - root-ca: Root Certificate Authority files +# - intermediate-ca: Intermediate Certificate Authority files +# - ocsp-responder: OCSP responder certificates +mkdir -p "${CERT_DIR}/ca/root-ca/private" \ + "${CERT_DIR}/ca/intermediate-ca/private" \ + "${CERT_DIR}/ca/ocsp-responder" +chmod 700 "${CERT_DIR}/ca/root-ca/private" "${CERT_DIR}/ca/intermediate-ca/private" + +# Initialize the certificate databases and serial number counters +# index.txt acts as a database of all certificates +# serial defines the next certificate serial number +echo -n > "${CERT_DIR}/ca/intermediate-ca/index.txt" +echo 1000 > "${CERT_DIR}/ca/intermediate-ca/serial" + +# Generate the Root CA private key and certificate +# This is the top-level certificate authority that signs the intermediate CA +openssl genrsa -out "${CERT_DIR}/ca/root-ca/private/ca.key" 4096 + +openssl req -new -x509 -key "${CERT_DIR}/ca/root-ca/private/ca.key" \ + -out "${CERT_DIR}/ca/root-ca/ca.crt" -days 3650 \ + -subj "/C=GB/ST=London/L=London/O=Test Openfire/CN=Test Openfire Root CA" + +# Generate the Intermediate CA private key and certificate signing request (CSR) +# The intermediate CA will be used to sign the server and OCSP responder certificates +openssl genrsa -out "${CERT_DIR}/ca/intermediate-ca/private/intermediate.key" 4096 + +openssl req -new -key "${CERT_DIR}/ca/intermediate-ca/private/intermediate.key" \ + -out "${CERT_DIR}/ca/intermediate-ca/intermediate.csr" \ + -subj "/C=GB/ST=London/L=London/O=Test Openfire/CN=Test Openfire Intermediate CA" + +# Sign the Intermediate CA certificate with the Root CA +# The certificate includes: +# - CA capabilities (basicConstraints) +# - Permission to sign certificates (keyUsage) +# - Location of the OCSP responder (authorityInfoAccess) +openssl x509 -req -in "${CERT_DIR}/ca/intermediate-ca/intermediate.csr" \ + -CA "${CERT_DIR}/ca/root-ca/ca.crt" \ + -CAkey "${CERT_DIR}/ca/root-ca/private/ca.key" -CAcreateserial \ + -out "${CERT_DIR}/ca/intermediate-ca/intermediate.crt" -days 1825 \ + -extfile <(printf 'basicConstraints=critical,CA:true,pathlen:0\nkeyUsage=critical,digitalSignature,keyCertSign,cRLSign\nauthorityInfoAccess=OCSP;URI:%s' "$OCSP_URL") + +# Function to generate a server certificate and add it to the certificate database +# Parameters: +# $1: instance number (1 or 2 for xmpp1/xmpp2) +generate_server_cert() { + local instance=$1 + + # Generate server private key and CSR + openssl genrsa -out "${CERT_DIR}/server${instance}.key" 2048 + + # Create OpenSSL config file for SAN support + cat > "${CERT_DIR}/server${instance}.cnf" << EOF +[req] +distinguished_name = req_distinguished_name +req_extensions = v3_req +prompt = no + +[req_distinguished_name] +C = GB +ST = London +L = London +O = Test Openfire +CN = xmpp${instance}.localhost.example + +[v3_req] +basicConstraints = CA:FALSE +keyUsage = critical, digitalSignature, keyEncipherment +extendedKeyUsage = serverAuth +subjectAltName = @alt_names + +[alt_names] +DNS.1 = xmpp${instance}.localhost.example +DNS.2 = *.xmpp${instance}.localhost.example +EOF + + # Generate CSR with the config file + openssl req -new -key "${CERT_DIR}/server${instance}.key" \ + -out "${CERT_DIR}/server${instance}.csr" \ + -config "${CERT_DIR}/server${instance}.cnf" + + # Create OpenSSL config for certificate signing + cat > "${CERT_DIR}/server${instance}_sign.cnf" << EOF +basicConstraints = critical,CA:false +keyUsage = critical,digitalSignature,keyEncipherment +extendedKeyUsage = serverAuth +subjectAltName = @alt_names +authorityInfoAccess = OCSP;URI:${OCSP_URL} + +[alt_names] +DNS.1 = xmpp${instance}.localhost.example +DNS.2 = *.xmpp${instance}.localhost.example +EOF + + # Sign the server certificate with the Intermediate CA + openssl x509 -req -in "${CERT_DIR}/server${instance}.csr" \ + -CA "${CERT_DIR}/ca/intermediate-ca/intermediate.crt" \ + -CAkey "${CERT_DIR}/ca/intermediate-ca/private/intermediate.key" -CAcreateserial \ + -out "${CERT_DIR}/server${instance}.crt" -days 365 \ + -extfile "${CERT_DIR}/server${instance}_sign.cnf" + + # Create the full certificate chain + cat "${CERT_DIR}/server${instance}.crt" "${CERT_DIR}/ca/intermediate-ca/intermediate.crt" "${CERT_DIR}/ca/root-ca/ca.crt" > "${CERT_DIR}/chain${instance}.pem" + + # Add to certificate database + SERIAL=$(openssl x509 -in "${CERT_DIR}/server${instance}.crt" -noout -serial | cut -d'=' -f2) + SUBJECT=$(openssl x509 -in "${CERT_DIR}/server${instance}.crt" -noout -subject | cut -d'=' -f2-) + printf 'V\t%s\t\t%s\tunknown\t%s\n' "$(date -u +%y%m%d%H%M%SZ)" "$SERIAL" "$SUBJECT" >> "${CERT_DIR}/ca/intermediate-ca/index.txt" + + # Clean up temporary config files + rm -f "${CERT_DIR}/server${instance}.cnf" "${CERT_DIR}/server${instance}_sign.cnf" + + # Display the certificate's subject and SANs for verification + echo "Certificate generated for instance ${instance}:" + openssl x509 -in "${CERT_DIR}/server${instance}.crt" -noout -subject -ext subjectAltName +} + +# Generate the OCSP responder certificate +# This certificate will be used to sign OCSP responses +openssl genrsa -out "${CERT_DIR}/ca/ocsp-responder/ocsp.key" 2048 + +openssl req -new -key "${CERT_DIR}/ca/ocsp-responder/ocsp.key" \ + -out "${CERT_DIR}/ca/ocsp-responder/ocsp.csr" \ + -subj "/C=GB/ST=London/L=London/O=Test Openfire/CN=ocsp.example.com" + +# Sign the OCSP responder certificate +# The certificate includes: +# - Non-CA status (basicConstraints) +# - OCSP signing capability (extendedKeyUsage) +openssl x509 -req -in "${CERT_DIR}/ca/ocsp-responder/ocsp.csr" \ + -CA "${CERT_DIR}/ca/intermediate-ca/intermediate.crt" \ + -CAkey "${CERT_DIR}/ca/intermediate-ca/private/intermediate.key" -CAcreateserial \ + -out "${CERT_DIR}/ca/ocsp-responder/ocsp.crt" -days 365 \ + -extfile <(printf 'basicConstraints=critical,CA:false\nkeyUsage=critical,digitalSignature\nextendedKeyUsage=OCSPSigning') + +# Generate certificates for both Openfire instances +generate_server_cert 1 +generate_server_cert 2 + +# Verify the certificate chain for all generated certificates +# This ensures that all certificates are properly signed and trusted +echo "Verifying certificates..." +openssl verify -CAfile "${CERT_DIR}/ca/root-ca/ca.crt" "${CERT_DIR}/ca/intermediate-ca/intermediate.crt" +openssl verify -CAfile <(cat "${CERT_DIR}/ca/root-ca/ca.crt" "${CERT_DIR}/ca/intermediate-ca/intermediate.crt") "${CERT_DIR}/server1.crt" +openssl verify -CAfile <(cat "${CERT_DIR}/ca/root-ca/ca.crt" "${CERT_DIR}/ca/intermediate-ca/intermediate.crt") "${CERT_DIR}/server2.crt" +openssl verify -CAfile <(cat "${CERT_DIR}/ca/root-ca/ca.crt" "${CERT_DIR}/ca/intermediate-ca/intermediate.crt") "${CERT_DIR}/ca/ocsp-responder/ocsp.crt" + +printf '\nCertificate generation complete.' +printf '\nOCSP URL configured as: %s\n\n' "$OCSP_URL" \ No newline at end of file diff --git a/federation/scripts/import-certificates.sh b/federation/scripts/import-certificates.sh new file mode 100755 index 0000000..2433d07 --- /dev/null +++ b/federation/scripts/import-certificates.sh @@ -0,0 +1,126 @@ +#!/bin/bash + +# Default password for all keystores and truststores +# This is Openfire's default password - changing it would require updating security.xml +KEYSTORE_PASSWORD="changeit" + +# Base directory where certificates were generated +# This should match the CERT_DIR from the certificate generation script +CERT_DIR="./_data/certs" + +# Function to import certificates and set up keystores/truststores for one Openfire instance +# Parameters: +# $1: instance number (1 or 2 for xmpp1/xmpp2) +import_certificates() { + local instance=$1 + + # Directory where Openfire expects to find its certificates + local conf_dir="_data/xmpp/${instance}/conf/security" + + echo "Importing certificates for Openfire instance ${instance}..." + + # Ensure the security directory exists + mkdir -p "${conf_dir}" + + # Remove any existing keystore to start fresh + rm -f "${conf_dir}/keystore" + + echo "Creating new keystore for instance ${instance}" + # Create a new empty keystore by generating and immediately deleting a temporary keypair + # This ensures the keystore is properly initialized with the correct format + keytool -genkeypair \ + -keystore "${conf_dir}/keystore" \ + -storepass "${KEYSTORE_PASSWORD}" \ + -keypass "${KEYSTORE_PASSWORD}" \ + -alias "default" \ + -dname "CN=temporary" \ + -keyalg RSA \ + -validity 1 + # Remove the temporary keypair, leaving an empty keystore + keytool -delete \ + -alias "default" \ + -keystore "${conf_dir}/keystore" \ + -storepass "${KEYSTORE_PASSWORD}" + + # Import the server's certificate and private key + # First convert them to PKCS12 format which can be imported into a Java keystore + echo "Importing server certificate and private key..." + openssl pkcs12 -export \ + -in "${CERT_DIR}/server${instance}.crt" \ + -inkey "${CERT_DIR}/server${instance}.key" \ + -chain \ + -CAfile "${CERT_DIR}/chain${instance}.pem" \ + -name "xmpp${instance}.localhost.example" \ + -out "${CERT_DIR}/server${instance}.p12" \ + -password "pass:${KEYSTORE_PASSWORD}" + + # Import the PKCS12 file into the keystore + keytool -importkeystore \ + -deststorepass "${KEYSTORE_PASSWORD}" \ + -destkeypass "${KEYSTORE_PASSWORD}" \ + -destkeystore "${conf_dir}/keystore" \ + -srckeystore "${CERT_DIR}/server${instance}.p12" \ + -srcstoretype PKCS12 \ + -srcstorepass "${KEYSTORE_PASSWORD}" \ + -alias "xmpp${instance}.localhost.example" + + # Create or update truststore + # truststore - used by the server to verify other servers + if [ ! -f "${conf_dir}/truststore" ]; then + echo "Creating new truststore for instance ${instance}" + # Initialize a new truststore with a dummy entry + keytool -importpass \ + -keystore "${conf_dir}/truststore" \ + -storepass "${KEYSTORE_PASSWORD}" \ + -storetype JKS \ + -alias "init" \ + -noprompt + fi + + # Add our CA certificates to truststore + # We preserve existing entries to maintain trust for other certificates + echo "Adding CA certificates to truststore..." + + # Import root CA if not already present + # grep -q checks if the alias already exists in the store + if ! keytool -list -keystore "${conf_dir}/truststore" -storepass "${KEYSTORE_PASSWORD}" | grep -q "root-ca"; then + keytool -import -noprompt \ + -keystore "${conf_dir}/truststore" \ + -storepass "${KEYSTORE_PASSWORD}" \ + -alias "root-ca" \ + -file "${CERT_DIR}/ca/root-ca/ca.crt" + echo "Added root CA to truststore" + else + echo "Root CA already exists in truststore" + fi + + # Import intermediate CA if not already present + if ! keytool -list -keystore "${conf_dir}/truststore" -storepass "${KEYSTORE_PASSWORD}" | grep -q "intermediate-ca"; then + keytool -import -noprompt \ + -keystore "${conf_dir}/truststore" \ + -storepass "${KEYSTORE_PASSWORD}" \ + -alias "intermediate-ca" \ + -file "${CERT_DIR}/ca/intermediate-ca/intermediate.crt" + echo "Added intermediate CA to truststore" + else + echo "Intermediate CA already exists in truststore" + fi + + echo "Certificate import completed for instance ${instance}" +} + +# Main script execution + +echo "Starting certificate import process..." + +# Import certificates for both Openfire instances +import_certificates 1 +import_certificates 2 + +# Clean up temporary PKCS12 files that were created during the import process +rm -f "${CERT_DIR}"/*.p12 + +# Display commands that can be used to verify the keystore contents +echo -e "\nTo verify the keystores, run:" +echo "keytool -list -keystore _data/xmpp/1/conf/security/keystore -storepass ${KEYSTORE_PASSWORD}" +echo "keytool -list -keystore _data/xmpp/1/conf/security/truststore -storepass ${KEYSTORE_PASSWORD}" \ No newline at end of file diff --git a/federation/start.sh b/federation/start.sh index 607cd04..004f55e 100755 --- a/federation/start.sh +++ b/federation/start.sh @@ -1,8 +1,9 @@ #!/bin/bash -usage() { echo "Usage: $0 [-n openfire-tag] [-6] [-h] +usage() { echo "Usage: $0 [-n openfire-tag] [-6] [-o] [-h] -n openfire-tag Launches all Openfire instances with the specified tag. This overrides the value in .env -6 Replace standard IPv4-based bridge networking with IPv6. + -o Enable OCSP support, generates compatible certificates, & deploys associated OCSP responder -h Show this helpful information "; exit 0; } @@ -20,7 +21,7 @@ source "$SCRIPTPATH/../_common/functions.sh" check_deps -while getopts n:6h o; do +while getopts n:6oh o; do case "$o" in n) if [[ $OPTARG =~ " " ]]; then @@ -34,6 +35,10 @@ while getopts n:6h o; do echo "Using IPv6" NETWORK_COMPOSE_FILE="docker-compose-network-dualstack.yml" ;; + o) + echo "Enabling OCSP support" + export ENABLE_OCSP=true + ;; h) usage ;; @@ -60,5 +65,12 @@ mkdir _data cp -r xmpp _data/ cp -r plugins _data/ +if [ "$ENABLE_OCSP" = true ]; then + echo "Enabling OCSP support" + "$SCRIPTPATH"/scripts/generate-certificates.sh + "$SCRIPTPATH"/scripts/import-certificates.sh + COMPOSE_FILE_COMMAND+=("-f" "docker-compose-ocsp-responder.yml") +fi + "${COMPOSE_FILE_COMMAND[@]}" up -d || popd popd \ No newline at end of file