4.3 SSL Certificate Generation


Key Generation Shell Script

The Dataspace Testbed uses a shell script on the VM to generate new certificates. The script is shown below:

Click here to expand script
#!/bin/bash
# Regenerate certificates and configure truststores for IDS Reference Testbed
set -e  # Exit on any error

# Usage function
usage() {
    echo "Usage: $0 [mode]"
    echo "Modes:"
    echo "  full      - Execute complete script (default)"
    echo "  daps      - CA, subCA and DAPS certificates only"
    echo "  broker    - Broker certificates and keystore only"
    echo "  combined  - DAPS + Broker (daps + broker)"
    echo "  connector - Add new connector (requires connector name as second parameter, optional IP address as third parameter)"
    echo ""
    echo "Examples:"
    echo "  $0 full"
    echo "  $0 daps"
    echo "  $0 broker"
    echo "  $0 combined"
    echo "  $0 connector connectorC"
    echo "  $0 connector connectorC 192.168.1.100"
    exit 1
}

# Configuration
ROOT_DIR=/home/ec2-user/IDS-testbed
SSL_DIR=data-cfssl
SERVER_ADDR=127.0.0.1
MODE=${1:-full}

# Validate mode
case "$MODE" in
    full|daps|broker|combined|connector)
        ;;
    *)
        echo "ERROR: Invalid mode '$MODE'"
        usage
        ;;
esac

# For connector mode, validate connector name and optional IP
if [ "$MODE" = "connector" ]; then
    if [ -z "$2" ]; then
        echo "ERROR: Connector name required for connector mode"
        usage
    fi
    CONNECTOR_NAME=$2
    CONNECTOR_IP=$3  # Optional IP address
fi

echo "Starting certificate regeneration process in '$MODE' mode..."

# Function: Setup PKI (always needed)
setup_pki() {
    echo "Setting up PKI infrastructure..."
    cd $ROOT_DIR/CertificateAuthority
    [ -d "$SSL_DIR" ] && rm -rf "$SSL_DIR"
    sh setup_PKI.sh $SSL_DIR
    
    if [ ! -d "$SSL_DIR/certs" ]; then
        echo "ERROR: Certificate generation failed - certs directory not found"
        exit 1
    fi
}

# Function: Process CA and subCA certificates
process_ca_certificates() {
    echo "Processing CA and subCA certificates..."
    cd $ROOT_DIR/CertificateAuthority/$SSL_DIR/ca
    openssl pkcs12 -export -out ca.p12 -in ca.pem -inkey ca-key.pem -passout pass:password
    openssl pkcs12 -in ca.p12 -clcerts -nokeys -out ca.crt -passin pass:password
    openssl pkcs12 -in ca.p12 -out ca.cert -nokeys -nodes -passin pass:password
    cp ca-key.pem ca.key
    
    cd $ROOT_DIR/CertificateAuthority/$SSL_DIR/subca
    openssl pkcs12 -export -out subca.p12 -in subca.pem -inkey subca-key.pem -passout pass:password
    openssl pkcs12 -in subca.p12 -clcerts -nokeys -out subca.crt -passin pass:password
    openssl pkcs12 -in subca.p12 -out subca.cert -nokeys -nodes -passin pass:password
    cp subca-key.pem subca.key
}

# Function: Process DAPS certificates
process_daps_certificates() {
    echo "Processing DAPS certificates..."
    cd $ROOT_DIR/CertificateAuthority/$SSL_DIR/certs
    
    # DAPS certificates
    openssl pkcs12 -export -out daps.p12 -in daps.pem -inkey daps-key.pem -passout pass:password
    openssl pkcs12 -in daps.p12 -clcerts -nokeys -out daps.crt -passin pass:password
    openssl pkcs12 -in daps.p12 -out daps.cert -nokeys -nodes -passin pass:password
    cp daps-key.pem daps.key
    
    # Copy DAPS certificates
    cp daps.cert $ROOT_DIR/DAPS/keys/TLS/daps.cert
    cp daps.key $ROOT_DIR/DAPS/keys/TLS/daps.key
    cp daps.key $ROOT_DIR/DAPS/keys/omejdn/omejdn.key
}

# Function: Process broker certificates
process_broker_certificates() {
    echo "Processing broker certificates..."
    cd $ROOT_DIR/CertificateAuthority/$SSL_DIR/certs
    
    # Broker certificates
    openssl pkcs12 -export -out broker.p12 -in broker.pem -inkey broker-key.pem -passout pass:password
    openssl pkcs12 -in broker.p12 -clcerts -nokeys -out broker.crt -passin pass:password
    openssl pkcs12 -in broker.p12 -out broker.cert -nokeys -nodes -passin pass:password
    cp broker-key.pem broker.key
    
    # Create broker keystore
    rm -f $ROOT_DIR/MetadataBroker/isstbroker-keystore.jks
    keytool -importkeystore -srckeystore broker.p12 -srcstoretype PKCS12 -srcstorepass password -destkeystore $ROOT_DIR/MetadataBroker/isstbroker-keystore.jks -deststoretype JKS -deststorepass password -noprompt
    
    # Copy broker certificates
    cp broker.cert $ROOT_DIR/DAPS/keys/broker.cert
    cp broker.key $ROOT_DIR/MetadataBroker/server.key
    cp broker.crt $ROOT_DIR/MetadataBroker/server.crt
}

# Function: Process connector certificates (for full mode)
process_connector_certificates() {
    echo "Processing connector certificates..."
    cd $ROOT_DIR/CertificateAuthority/$SSL_DIR/certs
    
    # ConnectorA certificates
    openssl pkcs12 -export -out connectorA.p12 -in connectorA.pem -inkey connectorA-key.pem -passout pass:password
    openssl pkcs12 -in connectorA.p12 -clcerts -nokeys -out connectorA.crt -passin pass:password
    openssl pkcs12 -in connectorA.p12 -out connectorA.cert -nokeys -nodes -passin pass:password
    cp connectorA-key.pem connectorA.key
    
    # ConnectorB certificates
    openssl pkcs12 -export -out connectorB.p12 -in connectorB.pem -inkey connectorB-key.pem -passout pass:password
    openssl pkcs12 -in connectorB.p12 -clcerts -nokeys -out connectorB.crt -passin pass:password
    openssl pkcs12 -in connectorB.p12 -out connectorB.cert -nokeys -nodes -passin pass:password
    cp connectorB-key.pem connectorB.key
    

    
    # Copy connector certificates
    cp connectorA.cert $ROOT_DIR/DAPS/keys/connectorA.cert
    cp connectorB.cert $ROOT_DIR/DAPS/keys/connectorB.cert
    cp connectorA.p12 $ROOT_DIR/DataspaceConnectorA/conf/connectorA.p12
    cp connectorB.p12 $ROOT_DIR/DataspaceConnectorB/conf/connectorB.p12
}

# Function: Create truststores
create_truststores() {
    echo "Creating truststores..."
    rm -f $ROOT_DIR/DataspaceConnectorA/conf/truststore.p12
    rm -f $ROOT_DIR/DataspaceConnectorB/conf/truststore.p12
    
    keytool -import -alias testbedca -file $ROOT_DIR/CertificateAuthority/$SSL_DIR/ca/ca.crt -storetype PKCS12 -keystore $ROOT_DIR/DataspaceConnectorA/conf/truststore.p12 -storepass password -noprompt
    keytool -import -alias testbedsubca -file $ROOT_DIR/CertificateAuthority/$SSL_DIR/subca/subca.crt -storetype PKCS12 -keystore $ROOT_DIR/DataspaceConnectorA/conf/truststore.p12 -storepass password -noprompt
    keytool -import -alias testbedca -file $ROOT_DIR/CertificateAuthority/$SSL_DIR/ca/ca.crt -storetype PKCS12 -keystore $ROOT_DIR/DataspaceConnectorB/conf/truststore.p12 -storepass password -noprompt
    keytool -import -alias testbedsubca -file $ROOT_DIR/CertificateAuthority/$SSL_DIR/subca/subca.crt -storetype PKCS12 -keystore $ROOT_DIR/DataspaceConnectorB/conf/truststore.p12 -storepass password -noprompt
}

# Function: Register with DAPS
register_daps() {
    cd $ROOT_DIR/DAPS
    case "$MODE" in
        full)
            rm -f keys/clients/*.cert
            echo "---" > config/clients.yml

            sh ./register_connector.sh broker
            sh ./register_connector.sh connectorA
            sh ./register_connector.sh connectorB
            ;;
        daps)
            rm -f keys/clients/*.cert
            echo "---" > config/clients.yml
            # DAPS only - no registration needed
            ;;
        broker)
            sh ./register_connector.sh broker
            ;;
        combined)
            sh ./register_connector.sh broker
            ;;
        connector)
            sh ./register_connector.sh "$CONNECTOR_NAME"
            ;;
    esac
}

# Function: Add new connector (from add-connector.sh logic)
add_connector() {
    echo "Adding new connector: $CONNECTOR_NAME"
    
    # Check if configuration file exists, create if not
    if [ ! -f "$ROOT_DIR/CertificateAuthority/pkiInput/${CONNECTOR_NAME}.json" ]; then
        echo "Creating configuration file for $CONNECTOR_NAME"
        
        cat > "$ROOT_DIR/CertificateAuthority/pkiInput/${CONNECTOR_NAME}.json" <<EOF
{
  "CN": "Connector ${CONNECTOR_NAME^^}",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
  {
    "C": "DE",
    "L": "Dortmund",
    "O": "IDSA",
    "OU": "IDS Reference Testbed"
  }
  ],
  "hosts": [
    "localhost",
    "${CONNECTOR_NAME,,}",
    "127.0.0.1"$([ -n "$CONNECTOR_IP" ] && echo ",")
    $([ -n "$CONNECTOR_IP" ] && echo "\"$CONNECTOR_IP\"")
  ]
}
EOF
    else
        echo "Configuration file already exists for $CONNECTOR_NAME, skipping creation"
    fi

    # Generate certificate for new connector
    cd "$ROOT_DIR/CertificateAuthority/$SSL_DIR/certs"
    
    cfssl genkey "$ROOT_DIR/CertificateAuthority/pkiInput/${CONNECTOR_NAME}.json" | cfssljson -bare "$CONNECTOR_NAME"
    cfssl sign -ca "$ROOT_DIR/CertificateAuthority/$SSL_DIR/subca/subca.pem" \
        -ca-key "$ROOT_DIR/CertificateAuthority/$SSL_DIR/subca/subca-key.pem" \
        -db-config "$ROOT_DIR/CertificateAuthority/$SSL_DIR/ocsp/sqlite_db_components.json" \
        --config "$ROOT_DIR/CertificateAuthority/pkiInput/ca-config.json" \
        -profile "component" "${CONNECTOR_NAME}.csr" | cfssljson -bare "$CONNECTOR_NAME"
    
    # Convert to required formats
    openssl pkcs12 -export -out "${CONNECTOR_NAME}.p12" -in "${CONNECTOR_NAME}.pem" -inkey "${CONNECTOR_NAME}-key.pem" -passout pass:password
    openssl pkcs12 -in "${CONNECTOR_NAME}.p12" -clcerts -nokeys -out "${CONNECTOR_NAME}.crt" -passin pass:password
    openssl pkcs12 -in "${CONNECTOR_NAME}.p12" -out "${CONNECTOR_NAME}.cert" -nokeys -nodes -passin pass:password
    cp "${CONNECTOR_NAME}-key.pem" "${CONNECTOR_NAME}.key"
    
    # Create dedicated certificate directory
    CERT_DIR="$ROOT_DIR/Certificates-${CONNECTOR_NAME}"
    mkdir -p "$CERT_DIR"
    
    cp "${CONNECTOR_NAME}.p12" "$CERT_DIR/connector.p12"
    cp "${CONNECTOR_NAME}.cert" "$CERT_DIR/connector.cert"
    cp "${CONNECTOR_NAME}.crt" "$CERT_DIR/connector.crt"
    cp "${CONNECTOR_NAME}.key" "$CERT_DIR/connector.key"
    cp "${CONNECTOR_NAME}.pem" "$CERT_DIR/connector.pem"
    
    # Create truststore in certificate directory
    keytool -import -alias testbedca -file "$ROOT_DIR/CertificateAuthority/$SSL_DIR/ca/ca.crt" -storetype PKCS12 -keystore "$CERT_DIR/truststore.p12" -storepass password -noprompt
    keytool -import -alias testbedsubca -file "$ROOT_DIR/CertificateAuthority/$SSL_DIR/subca/subca.crt" -storetype PKCS12 -keystore "$CERT_DIR/truststore.p12" -storepass password -noprompt

    chmod 666 "$CERT_DIR/connector.p12"
    chmod 666 "$CERT_DIR/connector.cert"
    chmod 666 "$CERT_DIR/connector.crt"
    chmod 666 "$CERT_DIR/connector.key"
    chmod 666 "$CERT_DIR/connector.pem"
    chmod 666 "$CERT_DIR/truststore.p12"
    
    # Copy certificate to DAPS for registration
    cp "${CONNECTOR_NAME}.cert" "$ROOT_DIR/DAPS/keys/${CONNECTOR_NAME}.cert"
    
    echo "Connector $CONNECTOR_NAME added successfully!"
    echo "Certificate directory created: $CERT_DIR"
}

# Function: Verify certificates
verify_certificates() {
    echo "Verifying certificate generation..."
    
    case "$MODE" in
        full)
            for cert in broker connectorA connectorB; do
                if [ ! -f "$ROOT_DIR/DAPS/keys/${cert}.cert" ]; then
                    echo "ERROR: Missing certificate: $ROOT_DIR/DAPS/keys/${cert}.cert"
                    exit 1
                fi
            done
            if [ ! -f "$ROOT_DIR/MetadataBroker/isstbroker-keystore.jks" ]; then
                echo "ERROR: Missing broker keystore"
                exit 1
            fi
            ;;
        daps)
            if [ ! -f "$ROOT_DIR/DAPS/keys/TLS/daps.cert" ]; then
                echo "ERROR: Missing DAPS certificate"
                exit 1
            fi
            ;;
        broker)
            if [ ! -f "$ROOT_DIR/DAPS/keys/broker.cert" ]; then
                echo "ERROR: Missing broker certificate"
                exit 1
            fi
            if [ ! -f "$ROOT_DIR/MetadataBroker/isstbroker-keystore.jks" ]; then
                echo "ERROR: Missing broker keystore"
                exit 1
            fi
            ;;
        combined)
            if [ ! -f "$ROOT_DIR/DAPS/keys/TLS/daps.cert" ] || [ ! -f "$ROOT_DIR/DAPS/keys/broker.cert" ]; then
                echo "ERROR: Missing DAPS or broker certificates"
                exit 1
            fi
            ;;
        connector)
            if [ ! -f "$ROOT_DIR/DAPS/keys/${CONNECTOR_NAME}.cert" ]; then
                echo "ERROR: Missing connector certificate: $CONNECTOR_NAME"
                exit 1
            fi
            ;;
    esac
}

# Main execution logic
case "$MODE" in
    full)
        setup_pki
        process_ca_certificates
        process_daps_certificates
        process_broker_certificates
        process_connector_certificates
        create_truststores
        register_daps
        verify_certificates
        echo "Full certificate regeneration completed successfully!"
        ;;
    daps)
        setup_pki
        process_ca_certificates
        process_daps_certificates
        verify_certificates
        echo "DAPS certificate generation completed successfully!"
        ;;
    broker)
        setup_pki
        process_ca_certificates
        process_broker_certificates
        register_daps
        verify_certificates
        echo "Broker certificate generation completed successfully!"
        ;;
    combined)
        setup_pki
        process_ca_certificates
        process_daps_certificates
        process_broker_certificates
        register_daps
        verify_certificates
        echo "Combined DAPS+Broker certificate generation completed successfully!"
        ;;
    connector)
        # For connector mode, we need existing PKI
        if [ ! -d "$ROOT_DIR/CertificateAuthority/$SSL_DIR" ]; then
            echo "ERROR: PKI not found. Run with 'daps' or 'full' mode first."
            exit 1
        fi
        add_connector
        register_daps
        verify_certificates
        echo "Connector $CONNECTOR_NAME added successfully!"
        ;;
esac

echo "Operation completed. You can restart containers with: sudo docker compose down && sudo docker compose up -d"

This script can be called manually or from the dashboard through a handler Python script system service.

Handler Python Script

A Python script runs as a system service, acting as a simple server that handles requests from the dashboard and updates certificates. The script works with both the full testbed and standalone connector configurations.

Click here to expand script
#!/usr/bin/env python3
from flask import Flask, request, jsonify, send_file
import subprocess
import json
import os
import zipfile
import tempfile
import shutil
import re

def update_pki_input(filename, ip):
    filepath = os.path.join('CertificateAuthority', 'pkiInput', f'{filename}.json')
    with open(filepath, 'r') as f:
        data = json.load(f)
    
    if ip and ip not in data['hosts']:
        data['hosts'].append(ip)
    
    with open(filepath, 'w') as f:
        json.dump(data, f, indent=2)

app = Flask(__name__)

@app.route('/key-generation', methods=['POST'])
def key_generation():
    data = request.get_json()
    cmd = './genkey.sh'

    generateType = data.get('generateType')
    ip = data.get('ip')

    if generateType == 'full':
        update_pki_input('daps', ip)
        update_pki_input('broker', ip)
        update_pki_input('connectorA', ip)
        update_pki_input('connectorB', ip)
    elif generateType == 'connector':
        connectorName = data.get('connectorName')
        if not connectorName:
            return jsonify({'error': 'connectorName is required for connector generation'}), 400
        
        folder_path = f'Certificates-{connectorName}'
        if os.path.exists(folder_path):
            shutil.rmtree(folder_path)
        
        cmd += f' connector {connectorName}'
        if ip: cmd += f' {ip}'

    try:
        result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
        return jsonify({
            'stdout': result.stdout,
            'stderr': result.stderr,
            'returncode': result.returncode
        })
    except Exception as e:
        return jsonify({'error': str(e)}), 500
    
@app.route('/restart-containers', methods=['POST'])
def restart_containers():
    try:
        data = request.get_json(force=True, silent=True) or {}
        containers = data.get('containers')
        
        if containers:
            cmd = f"docker compose up -d {' '.join(containers)}"
        else:
            cmd = "docker compose down && docker compose up -d"
        
        result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
        return jsonify({
            'stdout': result.stdout,
            'stderr': result.stderr,
            'returncode': result.returncode
        })
    except Exception as e:
        return jsonify({'error': str(e)}), 500
    
@app.route('/load-keys', methods=['POST'])
def load_keys():
    try:
        zip_file = request.files['zip_file']
        target_directory = request.form['target_directory']
        files_to_load = json.loads(request.form['files_to_load'])
        
        with tempfile.TemporaryDirectory() as temp_dir:
            zip_path = os.path.join(temp_dir, 'upload.zip')
            zip_file.save(zip_path)
            
            with zipfile.ZipFile(zip_path, 'r') as zip_ref:
                zip_ref.extractall(temp_dir)
            
            os.makedirs(target_directory, exist_ok=True)
            
            for file_name in files_to_load:
                src_path = os.path.join(temp_dir, file_name)
                if os.path.exists(src_path):
                    shutil.copy2(src_path, target_directory)
        
        return jsonify({'success': True, 'message': 'Files loaded successfully'})
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/download-certificates/<connectorName>', methods=['GET'])
def download_certificates(connectorName):
    try:
        folder_path = f'Certificates-{connectorName}'
        if not os.path.exists(folder_path):
            return jsonify({'error': f'Folder {folder_path} not found'}), 404
        
        temp_zip = tempfile.NamedTemporaryFile(delete=False, suffix='.zip')
        try:
            with zipfile.ZipFile(temp_zip.name, 'w', zipfile.ZIP_DEFLATED) as zipf:
                for root, dirs, files in os.walk(folder_path):
                    for file in files:
                        file_path = os.path.join(root, file)
                        arcname = os.path.relpath(file_path, folder_path)
                        zipf.write(file_path, arcname)
            
            return send_file(temp_zip.name, as_attachment=True, download_name=f'{folder_path}.zip', mimetype='application/zip')
        finally:
            os.unlink(temp_zip.name)
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/list-certificates', methods=['GET'])
def list_certificates():
    try:
        pattern = re.compile(r'^Certificates-([a-zA-Z0-9]+)$')
        certificates = []
        
        for item in os.listdir('.'):
            if os.path.isdir(item):
                match = pattern.match(item)
                if match:
                    certificates.append(match.group(1))
        
        return jsonify(certificates)
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/configure-env', methods=['POST'])
def configure_env():
    try:
        values = request.get_json()
        env_vars = {}
        
        if os.path.exists('.env'):
            with open('.env', 'r') as f:
                for line in f:
                    if '=' in line:
                        key, value = line.strip().split('=', 1)
                        env_vars[key] = value
        
        env_vars.update(values)
        
        with open('.env', 'w') as f:
            for key, value in env_vars.items():
                f.write(f'{key}={value}\n')
        return jsonify({'success': True})
    except Exception as e:
        return jsonify({'error': str(e)}), 500

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=3333)

The system service file is shown below:

Click here to expand system service file
[Unit]
Description=Dashboard Operator Service
After=network.target
              
[Service]
Type=simple
User=root
WorkingDirectory=/home/ec2-user/IDS-testbed
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/var/lib/snapd/snap/bin
ExecStart=/opt/dashboard-operator/venv/bin/python /opt/dashboard-operator.py
Restart=always
RestartSec=10
              
[Install]
WantedBy=multi-user.target