Certificate Manager Dokumentation

Feature Beschreibung

Microservice zur Verwaltung von Partner- & Ausstellerzertifikaten im shared-client

Dieser Service dient der automatischen Pflege von Partner- & Ausstellerzertifikaten. Er kann Zertifikate im FSS speichern sowie von Ausstellern herunterladen.

Mit Hilfe eines Scripts kann der Import von LDIF-Dateien durchgeführt werden.

Import von Zertifikaten via REST API

Mit Hilfe der API POST /certificates können Zertifikate über den Certificate-Manager im FSS gespeichert werden. Der Certificate-Manager konfiguriert die Zertifikate automatisch, er erkennt z.B. die dem Zertifikat zugeordnete MPID sowie ob es sich um ein Partner- oder Ausstellerzertifikat handelt.

REST API

Für die API-Dokumentation wird Swagger verwendet.

Die URL für Swagger lautet: http://localhost:8070/aep-certificate-manager/swagger-ui.html

Import von LDIF Dateien via Script

Die Sub-CAs bieten LDIF Dateien an. Diese beinhalten Listen von Partnerzertifikaten.

Mit einem Script (internal) können LDIF Dateien heruntergeladen und dann über die API des Certificate-Managers in den FSS importiert werden.

Automatischer Zertifikatsimport auf Basis von AS4

Ein CronJob kann so konfiguriert werden, dass er in einem bestimmten Zeitintervall ausgeführt wird, um Partnerzertifikate durch Zugriff auf SubCA Server herunterzuladen. Der erste Schritt zur Automatisierung des Downloadvorgangs von Partnerzertifikaten besteht darin, die Partnerliste abzurufen. So wird sichergestellt, dass für alle Partner Zertifikate heruntergeladen werden.

Die Partnerliste wird aus dem AS4-Address-Service ermittelt:

  • Konfigurieren der Adressdienst-URL in application.properties. Stellen Sie sicher, dass die as4-Datenbank Partnerinformationen enthält
## AS4 Address Service
as4-address-service-url=
  • Alternativ können die Partner auch manuell konfiguriert werden. Konfigurieren Sie in application.yml die Liste der Partner-IDs.
cert-manager:
  partnerId: 990001,990002

NOTE: Konfigurieren Sie nicht beide gleichzeitig.

Die Liste der SubCA-URLs muss ebenfalls konfiguriert werden. Der CronJob greift auf alle konfigurierten URLs zu, um die Zertifikate herunterzuladen. Dies sollte in application.yml konfiguriert werden.

cert-manager:
  subca-url:
    - ldaps://subca1
    - ldaps://subca2

Die Adressen der Sub-CA sind in unserer internen Doku verfügbar.

Konfigurieren Sie das Cron-Timing des Schedulers mithilfe der folgenden Eigenschaft:

## Download Partner Certificates Scheduler (Set cron time)
download-partner-certificate-scheduler=0 15 14 * * *

Um den Scheduler zu deaktivieren, entfernen Sie entweder die Eigenschaft vollständig aus der Datei oder setzen Sie den Wert auf false, wie zum Beispiel:

## Download Partner Certificates Scheduler (Set cron time)
download-partner-certificate-scheduler=false

Automatischer LDIF Zertifikatsimport

Um die LDIF Zertifikate aller Sub-CA z.B. täglich zu aktualisieren kann das folgende Skript angepasst auf Ihr System zum Aufruf verwendet werden. Dieses kann per Cron-Configuration (Serverseitig oder durch einen entsprechenden Start-Pod in einem Kubernetes Cluster) getriggert ausgeführt werden.

Wichtig: Bitte überprüfen und pflegen Sie selbst die URLs der Sub-CAs für die LDIF Downloads, da diese sie aktuell häufiger ändern!

#!/bin/bash

# This script uploads certificates extracted from the ldif files downloaded from the configured subca urls.
# Usage e.g. : ./upload-certificate-ldif.sh

# Keycloak usage
# Ensure that the user's keycloak password and client secret is set in the following fields: PASSWORD, CLIENT_SECRET

# Pre-requisites: curl and unzip installed on same bash terminal - edit following constants as needed

# stop script execution on error
set -e

########### Keycloak configuration ###########
keycloakEnabled=false
KEYCLOAK_URL="http://host.docker.internal:9000/auth/realms/as4/protocol/openid-connect/token"
CLIENT_ID="certificate-manager-service"
CLIENT_SECRET=""
USERNAME="certificate-manager-service-user"
PASSWORD=""

# api for uploading certificate
UPLOAD_CERT_API="http://localhost:8080/aep-certificate-manager/certificates"

current_year=$(date +'%Y')
current_month=$(date +'%m')
current_day=$(date +'%d')

# urls for subca ldif files
SUBCA_URLs=(
  "https://crl.sm-pki.atos.net/emtMak_Atos-Smart-Grid.zip"
  "https://energyca.telesec.de/mak/MAK_T-Systems-EnergyCA.ldif"
  "https://www.countandcare.de/wp-content/uploads/$current_year/$current_month/MAK-$current_year$current_month$current_day.7z"
  "https://as-4-mako-sub-ca.da-rz.net/certs/MAK-latest.ldif"
  "https://www.schleupen.de/fileadmin/EWW/Dokumente/Smart_Meter_Gateway_Administration/Wirkbetrieb_AS4_Marktkommunikation_EMT.MAK/Schleupen_SE_SubCA_WIRK_$current_year-$current_month-$current_day.zip"
  "http://ldaps.smartserviceca.sm-pki.smartservice.de:8080/subca_ssca_emt_export.ldif"
  "https://ekn-energyca.telesec.de/mak/MAK_CA4Energy-EKN.ldif"
)

certKey="userCertificate;binary"

# function for access token
getAccessToken() {
  auth_response=$(curl --silent -X POST "$KEYCLOAK_URL" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=password" \
    -d "client_id=$CLIENT_ID" \
    -d "client_secret=$CLIENT_SECRET" \
    -d "username=$USERNAME" \
    -d "password=$PASSWORD" \
    -w "\n%{http_code}")

  auth_response_code=$(echo "$auth_response" | tail -n 1)
  echo "Response code of getting keycloak access token: $auth_response_code"

  if [ "$auth_response_code" != "200" ]; then
    rm -r ldif-data
    exit
  fi

  access_token=$(echo $auth_response | grep -o '"access_token":"[^"]*' | grep -o '[^"]*$')
  AUTHORIZATION_HEADER="Authorization: Bearer $access_token"

}

#function for uploading certificate
uploadCertificate() {
  echo -e "*** uploading certificate *** line number ${2}"
  local cert="$(sed -e 's/[[:space:]]*$//' <<<${1})"
  upload_cert_response=$(curl -s -o ldif-data/upload_cert_response.txt -w "%{http_code}" --location "$UPLOAD_CERT_API" \
    --header "$AUTHORIZATION_HEADER" \
    --header "Content-Type: application/json" \
    --data "{
    \"certificate\": \"$cert\"
    }
  }")
  if [ "$upload_cert_response" != "200" ]; then
    echo "error response from server: $upload_cert_response"
    if [[ "$upload_cert_response" == "401" && "$keycloakEnabled" == true ]]; then
      getAccessToken
    else
      exit
    fi
  else
    echo "response from server: $(cat ldif-data/upload_cert_response.txt)"
    #    cat ldif-data/upload_cert_response.txt
    echo
  fi
}

#function for parsing ldif file
parseFile() {
  echo -e "*** parsing ldif file ***\n"
  lineNo=0
  certLineNo=0
  cert=""
  isCertField=0

  sed -i 's/\r$//' "${1}"

  while read line; do

    ((lineNo = lineNo + 1))
    [ "${line:0:1}" == "#" ] && continue

    attr=${line%%:*}
    value=${line#*: }

    if [[ "$attr" == "$value" && "$isCertField" == "1" ]]; then
      cert="${cert}${value}"

    elif [ "$attr" == "$certKey" ]; then
      if [ -n "$cert" ]; then
        uploadCertificate "$cert" "$certLineNo"
      fi
      isCertField=1
      cert="$value"
      certLineNo="$lineNo"

    elif [ "$attr" != "$certKey" ]; then
      isCertField=0

    fi

  done <"${1}"

  if [ -n "$cert" ]; then
    uploadCertificate "$cert" "$certLineNo"

  fi
}

#function for setup and process file
processFile() {
  SUBCA_URL=${1}
  echo "file to download from url: $SUBCA_URL"
  filename="${SUBCA_URL##*/}"
  extension="${SUBCA_URL##*.}"
  if [ -d "ldif-tmp" ]; then
    rm -r ldif-tmp
  fi

  mkdir ldif-tmp
  cd ldif-tmp
  curl -O "$SUBCA_URL"

  if [ -f "$filename" ]; then
    if [ "$extension" == "ldif" ]; then
      mkdir ldif-data
      mv $filename ldif-data/
      parseFile "ldif-data/$filename"
    elif [ "$extension" == "zip" ]; then
      mkdir -p ldif-data
      if unzip -d ldif-data/ "$filename"; then
	for file in ldif-data/*; do
          filename="ldif-data/${file##*/}"
          parseFile "$filename"
        done
      else
        echo "Failed to unzip downloaded file $filename"
      fi
    elif [ "$extension" == "7z" ]; then
      mkdir -p ldif-data
      if 7z x "$filename" -o./ldif-data/; then
        for file in ldif-data/*; do
          filename="ldif-data/${file##*/}"
          parseFile "$filename"
        done
      else
        echo "Failed to 7-unzip downloaded file $filename"
      fi
    fi
    rm -rf ldif-data
  else
    echo "File does not exist."
  fi
  cd ..
  rm -r ldif-tmp
}

if [ "$keycloakEnabled" == "true" ]; then
  getAccessToken
else
  echo "Skipping token access"
  AUTHORIZATION_HEADER=""
fi

for SUBCA_URL in ${SUBCA_URLs[@]}; do
  processFile "$SUBCA_URL"
done

Automatische Pflege der AS4-Addresse

Darüber hinaus kann umgekehrt auch das AS4 System auf die Events des Certificate-Managers reagieren. Wurde ein Zertifikat gespeichert, kann auf dessen Basis automatisch die AS4-Adresse im AS4-System gepflegt werden.

Für weitere Details vergleichen Sie bitte die Message-Broker API.

Message-Broker API

Consumer: certificate.download.command.default

Beispiel:

{
    "as4Id": "9b618376-a93b-4be0-a11c-1f1946262c35",
    "delivered": "2023-10-01T12:00:00Z",
    "tenant": "9979046000004",
    "partner": "9903111000003",
    "url": "ldap://subca-server.de:389"
}

Das Platzieren einer Nachricht in dieser Warteschlange führt dazu, dass der Zertifikatsmanager das Zertifikat von der Sub-CA herunterlädt und dann im FSS speichert, sofern das Zertifikat bisher noch nicht im System hinterlegt ist. Auf dieses Vorgang können ggf. andere Systeme wieder reagieren, denn es wird ein Ereignis über die Exchange certificate.store.event hinterlegt.

Beispiel:

{
  "id": "9b618376-a93b-4be0-a11c-1f1946262c35",
  "created": "2023-10-01T12:00:00Z",
  "certificate": "MIICizCCAjGgAwIB...UoA=="
}

Weitere Informationen zur Architektur finden Sie intern.

Max Concurrency - Parallelität

Für jeden Consumer ist es möglich die Eigenschaft max-concurrency zu setzen. Diese definiert die maximale Anzahl von gleichzeitigen Verarbeitungsthreads, die für einen Consumer gestartet werden können. Der Default steht für jeden Consumer auf 50.

certificateDownloadCommandMaxConcurrency=50
certificateDownloadEventMaxConcurrency=50

Feature Abgrenzung

Die Verwaltung von Mandanten-Zertifikaten erfolgt über den CSR-Service.

Installation

Hardware Anforderungen

Eine Service-Instanz erfordert mindestens 512 MB RAM.

Wir empfehlen 0,1 CPU-Kerne je Instanz.

Run the Application

Um die Anwendung zu starten, können Sie Folgendes verwenden:

mvn spring-boot:run

Standardmäßig kann auf die Anwendung lokal zugegriffen werden über: http://localhost:8070

Um den Server-Port zu ändern, konfigurieren Sie den Server-Port in application.properties.

server.port=8070

Der API-Kontext ist wie folgt: http://localhost:8070/aep-certificate-manager

Actuator

Diese Root-URL für den Aktuator lautet: http://localhost:8070/aep-certificate-manager/actuator

Umsysteme

Es wird zwingend eine Anbindung eines Message-Brokers und des FSS benötigt. Vergleichen Sie hierzu die entsprechenden Abschnitte in der Konfiguration.

Optional kann ein AS4 System per Message-Broker angebunden werden. Vergleichen Sie hierzu den Abschnitt Message-Broker.

Konfiguration

FSS Connection

Der Service muss an den FSS angebunden werden.

Um eine Verbindung zu FSS herzustellen, müssen wir die URL in der folgenden Eigenschaft konfigurieren:

fss.server.api.url

Als FSS Client wird ein shared-client empfohlen. Dieser kann wie folgt konfiguriert werden:

client=shared-client

Anbindung Message Broker

Der Service muss an einen Message Broker angebunden werden.

Beispiel application.properties Konfiguration:

spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

Folgende Queues / Exchanges werden benötigt:

  • certificate.download.command.default (Queue)
  • certificate.download.event / certificate.download.event.default
  • certificate.store.event (Exchange)

Anbindung AS4 System

Zur Anbindung von Umsystemen (z.B. eines AS4-Systems) kann es nötig sein, die Queues/Exchanges anzupassen. Eine solche Experten-Konfiguration ist wie folgt möglich:

## Consumer Properties
certificateDownloadCommandExchange=certificate.download.command
certificateDownloadCommandGroup=default
certificateDownloadCommandExchangeType=topic
certificateDownloadCommandMaxConcurrency=50

certificateDownloadEventExchange=certificate.download.event
certificateDownloadEventGroup=default
certificateDownloadEventExchangeType=topic
certificateDownloadEventMaxConcurrency=50

consumer.max.retries=1

## Producer Properties
certificateStoreEventExchange=certificate.store.event
certificateStoreEventGroup=default

spring.cloud.function.definition=certificateDownloadCommandConsumer;certificateDownloadEventConsumer

spring.cloud.stream.bindings.certificateDownloadCommandConsumer-in-0.destination=${certificateDownloadCommandExchange}
spring.cloud.stream.bindings.certificateDownloadCommandConsumer-in-0.group=${certificateDownloadCommandGroup}
spring.cloud.stream.bindings.certificateDownloadCommandConsumer-in-0.consumer.max-attempts=${consumer.max.retries}
spring.cloud.stream.rabbit.bindings.certificateDownloadCommandConsumer-in-0.consumer.exchange-type=${certificateDownloadCommandExchangeType}
spring.cloud.stream.rabbit.bindings.certificateDownloadCommandConsumer-in-0.consumer.auto-bind-dlq=true
spring.cloud.stream.rabbit.bindings.certificateDownloadCommandConsumer-in-0.consumer.dlq-quorum.enabled=true
spring.cloud.stream.rabbit.bindings.certificateDownloadCommandConsumer-in-0.consumer.quorum.enabled=true
spring.cloud.stream.rabbit.bindings.certificateDownloadCommandConsumer-in-0.consumer.max-concurrency=${certificateDownloadCommandMaxConcurrency}

spring.cloud.stream.bindings.certificateDownloadEventConsumer-in-0.destination=${certificateDownloadEventExchange}
spring.cloud.stream.bindings.certificateDownloadEventConsumer-in-0.group=${certificateDownloadEventGroup}
spring.cloud.stream.bindings.certificateDownloadEventConsumer-in-0.consumer.max-attempts=${consumer.max.retries}
spring.cloud.stream.rabbit.bindings.certificateDownloadEventConsumer-in-0.consumer.exchange-type=${certificateDownloadEventExchangeType}
spring.cloud.stream.rabbit.bindings.certificateDownloadEventConsumer-in-0.consumer.auto-bind-dlq=true
spring.cloud.stream.rabbit.bindings.certificateDownloadEventConsumer-in-0.consumer.dlq-quorum.enabled=true
spring.cloud.stream.rabbit.bindings.certificateDownloadEventConsumer-in-0.consumer.quorum.enabled=true
spring.cloud.stream.rabbit.bindings.certificateDownloadEventConsumer-in-0.consumer.max-concurrency=${certificateDownloadEventMaxConcurrency}

spring.cloud.stream.bindings.certificateDownloadEventProducer-out-0.destination=${certificateDownloadEventExchange}
spring.cloud.stream.rabbit.bindings.certificateDownloadEventProducer-out-0.producer.exchange-type=topic

spring.cloud.stream.rabbit.bindings.certificateStoreEventProducer-out-0.producer.exchange-type=topic
spring.cloud.stream.bindings.certificateStoreEventProducer-out-0.destination=${certificateStoreEventExchange}

Folgende Schnittstellen bestehen zwischen dem AS4 System und dem Certificate Manager:

  • Trigger des Zertifikat-Downloads durch den Inbound Workflow
  • Aktualisierung der AS4-Adresse auf Basis von Zertifikaten

Trigger des Zertifikat-Downloads durch den AS4 Inbound Workflow

Verbinden Sie die exchange as4.receipt.create (AS4-Crypto-Operations) zusätzlich mit der queue certificate.download.command.default des Certificate Managers.

Aktualisierung der AS4-Adresse auf Basis von Zertifikaten

Verbinden Sie die Exchange certificate.store.event des Certificate Managers mit der Queue certificate.store.event.default des AS4-Address-Service.

Bitte stellen Sie sicher, dass die Queue existiert & mit der Exchange verbunden ist, bevor Sie die ersten Zertifikate über den Certificate-Manager importieren.

Expiration Konfiguration

Die geladenen Zertifikate werden validiert. Die neuen Zertifikate werden nur heruntergeladen, wenn die geladenen Zertifikate aus dem FSS fehlen, der Status „revoked“ ist oder sie bald ablaufen. Die Definition des baldigen Ablaufs der Zertifikate kann konfiguriert werden (in Tagen). Setzen Sie dafür folgende Property:

renew-certificate-days-before-expiration=14

Default: 14 Tage.

LDAPS Konfiguration

Es ist davon auszugehen, dass die LDAP-Systeme der Sub-CAs durch mTLS geschützt sind. Deshalb ist folgende Konfiguration zur Aktivierung von mTLS vorzunehmen:

ldaps.enabled=true
client=shared-client

Beim Client handelt es sich hierbei um den FSS-Client, in dem die Aussteller-Zertifikate hinterlegt sind. Per default ist automatisch der shared-client eingestellt.

Der Certificate-Manager muss für mTLS auch auf die Zertifikate der Mandanten zugreifen. Hierbei folgt er der Konvention, dass der Client des Mandanten der MPID des Mandanten entspricht. Dies ist somit Voraussetzung.

Keycloak Konfiguration

Falls Sie die REST-API des Certificate-Managers per oauth2 absichern möchten, oder falls die benötigte REST API des FSS per oauth2 gesichert ist, konfigurieren Sie Keycloak:

In der Keycloak-Admin-Oberfläche:

  • Ergänzen Sie einen neuen Client: certificate-manager-service
  • Aktivieren Sie am Client die Settings Client Authentication
  • Ergänzen Sie am Client den Tenant-Mapper
  • (Falls noch nicht geschehen,) Erstellen Sie einen Maschinen-User, der über die nötigen Rollen und Mandanten verfügt, um auf den FSS zuzugreifen.
  • Ergänzen Sie die notwendige(n) Rolle(n) für den Certificate-Manager an den Usern, die seine API aufrufen werden: * CERT-MANAGER-WRITE

Ergänzen Sie folgende Application-Properties um Keycloak zu aktivieren:

spring.profiles.active=keycloak-enriched
keycloak.enabled=true
keycloak.auth-server-url=https://<Ihr-Keycloak-Server>/auth
keycloak.realm=<Ihr-Keycloak-Realm>
keycloak.resource=certificate-manager-service
keycloak.credentials.secret=<Secret des Clients>
keycloakUsername=<Maschinen-User zum Zugriff auf den FSS>
keycloakPassword=<Passwort des Maschinen-Users>

Allgemeine Konfiguration

Weitere allgemeine Konfigurationsmöglichkeiten sind hier beschrieben.

View Me   Edit Me