Wer sich (beruflich) mit der IT im deutschen Gesundheitswesen beschäftigt, wird immer wieder mit Prozessen konfrontiert, die schon gänzlich zu Beginn ihres ersten Designentwurfes komplett an der Praxis vorbei geplant wurden.
Einer dieser Prozesse betrifft die Transport-PIN der SMC-B Karten für die Praxis, respektive eHBA für die Leistungserbringer. Vor der ersten Inbetriebnahme ist die Transport-PIN zu ändern ('change'), sowie später im Betrieb freizuschalten ('verify'). Um diese Änderungen oder Freischaltungen anzustoßen bedarf es eines SOAP Requests an den Telematik-Konnektor; die PIN Änderung sowie Freischaltung wird zwar am Kartenterminal durchgeführt, kann aber durch dieses nicht angestoßen werden.
Die Gematik sieht vor, dass hierfür die Primärsystemsoftware (Arztinformationssystem, Krankenhausinformationssystem, etc.) zuständig ist, d.h. von der Primärsystemsoftware wird ein SOAP Request an den Konnektor gesendet, der das Kartenterminal in den Modus versetzt, die PIN freizuschalten oder zu ändern.
Allerdings haben sich im Zuge der 'rasanten' Digitalisierung Situationen ergeben, bei der überhaupt kein Primärsystem existiert, aber dennoch ein Zugang zur Telematik und somit auch eine PIN-Freischaltung und -Änderung erforderlich sind. Das tritt u.a. in Situationen bei Privatärzten auf. Der Leistungserbringer verfügt dann nicht über die Möglichkeit, die PIN Änderung oder Freischaltung anzustoßen. Er hat viel Geld für die TI Komponenten ausgegeben, kann Sie aber nicht in Betrieb nehmen.
SOAP Requests
Das unten stehende Python-Tool ermöglicht das Absetzen von SOAP Requests für diese Pin-Funktionen zum Konnektor - ganz ohne Primärsystem.
- Anzeigen vorhandenen Karten für einen Mandanten
- getstatus, change, verify, unblock Kommandos ausführen
- Unterstützung für SMC-B Pin (PIN.SMC) und HBA Pin (PIN.CH und PIN.QES)
- Durch Nutzung der SOAP Schnittstelle ist die Freischaltung auch bei Rechenzentrumskonnektoren (Managed TI / TIaas) möglich.
Die hinterlegten SOAP Adressen sind bisher nur für die KocoBox und Secunet hinterlegt. Parameter --konnektor secu für Secunet benutzen.
Das Tool wird via Kommandozeile bedient und erwartet IP-Adresse, Infomodellparameter, TLS Zertifikate, eine ICCSN sowie die Operation als Eingabeparameter. Diese können direkt in einem Terminal übergeben werden, alternativ wird es über eine Batch-Datei aufgerufen. Es wird eine Python Laufzeitumgebung vorausgesetzt. Ein Doppelklick auf die Datei reicht nicht aus.
Konvertierung von Clientsystemzertifikaten im p12 Container
openssl pkcs12 -in certificate.p12 -out certificate.pem -nodes -legacy
Support
Sie können kommerziellen Support für dieses Programm für 250 € netto einmalig erwerben. Hiermit unterstützen Sie ebenfalls die Entwicklung zukünftiger, weiterer Tools dieser Art. Support beinhaltet:
- Beratung
- eine kompilierte .exe Datei für Windows
- die zur Verfügungstellung von Updates
- Unterstützung bei der Konvertierung der Zertifikate
- Unterstützung bei der Einrichtung auf einem Arbeitsplatz
- gesetzliche Gewährleistung
Ohne Supportpaket können Sie aufkommende Fragen im Forum der ti-community.de oder vondoczudoc.de stellen und sich von der Community helfen lassen. Eine Gewährleistung für das Programm, seine Lauffähigkeit oder die Haftung für sich hieraus ergebener Probleme wird ohne Support nicht gegeben.
Quellcode von pinguino.py
#!/usr/bin/env python3
#2025-06-04 by Christian Krause
import argparse
import re
import sys
import threading
import xml.dom.minidom
from base64 import b64encode
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
################## Base Functions #########################
def main(host, a):
K = Kon(host, a.mandant, a.clientsystem, a.arbeitsplatz, a.authMethod, a.proxy, a.usage, a.ICCSN, a.qes)
if K.getCardHandle():
K.pinMgmt()
def parseArgs():
p = argparse.ArgumentParser(description='Konnektor2CheckMK')
p.add_argument('-v', '--version', action='version', version='%(prog)s 1.0 mit API-Version: 1.1')
p.add_argument('-d', '--debug', action='store_true', help='No Sanity-Check. Just do it!')
p.add_argument('-p', '--proxy', action='store_true', help='Do not ignore system Proxy')
p.add_argument('-i', '--ip', dest='ipArray', nargs='+', metavar=("<IP/IP-Range/IP-Ranges>"), required=True, help='IP or IP-Range of Konnektor: 10.1.1.17 or 10.1.1.20-35')
p.add_argument('-m', '--mandant', required=True, metavar=("<Mandant>"))
p.add_argument('-c', '--clientsystem', required=True, metavar=("<Clientsystem>"))
p.add_argument('-a', '--arbeitsplatz', required=True, metavar=("<Arbeitsplatz>"))
p.add_argument('-s', '--authmethod', dest='authMethod', default="https", nargs='+', metavar=("<Auth Mechanism> [FileName] or <Auth Mechanism [<User> <Pass>]"), help='Auth-Mechanism: http, https, auth, cert')
p.add_argument('-u', '--usage', metavar=("usage"), choices=['status', 'verify', 'unblock', 'change'], help='Chose from status, verify, unblock, change, changeq (changes HBA QES Pin)')
p.add_argument('-I', '--ICCSN', metavar=("iccsn"), help='Specify an ICCSN for status, verify, unblock or change')
p.add_argument('-q', '--qes', action='store_true', help='Only for HBA: Use PIN.QES instead of PIN.CH')
return p.parse_args()
def initializeDicts():
# {"inputVar": ["Fehlermeldung", "Prüf-Regex"]}
global sanitizingDict
sanitizingDict = {
"ipArray": ["Ungültige IP-Adresse/IPRange! Range z.B. 192.168.0.17-24", r"^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(-25[0-5]|-2[0-4][0-9]|-1[0-9][0-9]|-[1-9]?[0-9])?$"],
"mandant": ["Mandant: Ungültige Zeichen oder Zeichenkette länger als 64 Zeichen! Gültige Zeichen: A-Z a-z 0-9 öÖäÄüÜß @#_-§%+:=.", r"^[a-zA-Z0-9öÖäÄüÜß@#_\-§%+:=\.]{1,64}$"],
"clientsystem": ["ClientSystem: Ungültige Zeichen oder Zeichenkette länger als 64 Zeichen! Gültige Zeichen: A-Z a-z 0-9 öÖäÄüÜß @#_-§%+:=.", r"^[a-zA-Z0-9öÖäÄüÜß@#_\-§%+:=\.]{1,64}$"],
"arbeitsplatz": ["Arbeitsplatz: Ungültige Zeichen oder Zeichenkette länger als 64 Zeichen! Gültige Zeichen: A-Z a-z 0-9 öÖäÄüÜß @#_-§%+:=.", r"^[a-zA-Z0-9öÖäÄüÜß@#_\-§%+:=\.]{1,64}$"],
"authMethod": ["", r"^(http|https|cert|auth)"],
"ICCSN": ["ICCSN Format: 20 stellige Ziffer beginnend mit 80276", r"^(80276)[0-9]{15}$"],
}
global getCardHandle
getCardHandle = """
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ev="http://ws.gematik.de/conn/EventService/v7.2"
xmlns:co="http://ws.gematik.de/conn/ConnectorContext/v2.0"
xmlns:cc="http://ws.gematik.de/conn/ConnectorCommon/v5.0">
<soapenv:Body>
<ev:GetCards mandant-wide="true">
<co:Context>
<cc:MandantId>%s</cc:MandantId>
<cc:ClientSystemId>%s</cc:ClientSystemId>
<cc:WorkplaceId>%s</cc:WorkplaceId>
</co:Context>
</ev:GetCards>
</soapenv:Body>
</soapenv:Envelope>
"""
global pinBody
pinBody = """
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:v8="http://ws.gematik.de/conn/CardService/v8.1"
xmlns:v2="http://ws.gematik.de/conn/ConnectorContext/v2.0"
xmlns:v5="http://ws.gematik.de/conn/ConnectorCommon/v5.0"
xmlns:v21="http://ws.gematik.de/conn/CardServiceCommon/v2.0">
<soapenv:Body>
<v8:%s>
<v2:Context>
<v5:MandantId>%s</v5:MandantId>
<v5:ClientSystemId>%s</v5:ClientSystemId>
<v5:WorkplaceId>%s</v5:WorkplaceId>
<!--Optional:-->
<v5:UserId>%s</v5:UserId>
</v2:Context>
<v5:CardHandle>%s</v5:CardHandle>
<v21:PinTyp>%s</v21:PinTyp>
</v8:%s>
</soapenv:Body>
</soapenv:Envelope>
"""
global functionDict
functionDict = {
"status" : "GetPinStatus",
"verify" : "VerifyPin",
"unblock" : "UnblockPin",
"change" : "ChangePin"
}
def ipRange(ipRanges):
ipArray = []
for entry in ipRanges:
if '-' in entry:
fromIP, toOctet = entry.rsplit("-", 1)
ipBase, fromOctet = fromIP.rsplit(".", 1)
if int(toOctet) <= int(fromOctet):
print('"Bis"-Angabe im Konnektor-Range muss größer sein als "Von"-Angabe!')
sys.exit()
ipArray += [f'{ipBase}.{i}' for i in range(int(fromOctet), int(toOctet) + 1)]
else:
ipArray.append(entry)
return ipArray
def Sanitizing():
for inputVar in vars(args):
if not (inputValue := getattr(args, inputVar)):
continue
if not isinstance(inputValue, list):
inputValue = [inputValue]
# Prüfe Regex für authMethod Parameter
if inputVar == "authMethod":
message, regex = sanitizingDict.get(inputVar)
if not re.search(regex, inputValue[0]):
print("Ungültige Authentifizierungsmethode: http, https, auth, cert")
return False
if inputValue[0] == "auth" and len(inputValue) != 3:
print("Bitte Benutzername und Kennwort als Parameter eingeben!")
return False
if inputValue[0] == "cert" and len(inputValue) != 2:
print("Bitte Pfad der Zertifikatsdatei angeben (Dateiname: globalcert.pem und globalcert.txt)")
return False
# Prüfe Regex für alles andere
elif inputVar in sanitizingDict:
message, regex = sanitizingDict.get(inputVar)
for inputString in inputValue:
if not re.search(regex, inputString):
print(message)
return False
return True
################## Kon Class #########################
class Kon:
def __init__(self, host, Mandant, ClientSystem, Arbeitsplatz, authMethod, Proxy, Usage, ICCSN, QES):
self.host = host
self.Mandant = Mandant
self.ClientSystem = ClientSystem
self.Arbeitsplatz = Arbeitsplatz
self.headers = {}
self.Card = {}
self.QES = bool(QES)
self.session = requests.session()
self.session.trust_env = Proxy
self.iccsn = ICCSN
# Set Auth Parameter
self.URL = f'https://{host}:443'
self.cert = None
self.Auth = None
if authMethod[0] == 'http':
self.URL = f'http://{host}:80'
elif authMethod[0] == "cert":
self.cert = authMethod[1]
elif authMethod[0] == "auth":
credentials = f'{authMethod[1]}:{authMethod[2]}'
self.Auth = 'Basic ' + b64encode(credentials.encode()).decode('ascii')
if not ICCSN:
self.usage = 'showcards'
elif not Usage:
self.usage = 'GetPinStatus'
else:
self.usage = functionDict[Usage]
def konComm(self, Service, data=None):
Message = ""
try:
if self.Auth:
self.headers.update({"Authorization": self.Auth})
if data:
self.response = self.session.post(f'{self.URL}{Service}', timeout=30, headers=self.headers, verify=False, cert=self.cert, data=data)
else:
self.response = self.session.get(f'{self.URL}{Service}', timeout=30, headers=self.headers, verify=False, cert=self.cert)
if self.response.status_code != 200:
Message = f'[{self.host}] HTTP Status Code: {self.response.status_code}'
print(Message)
return False
return True
except requests.exceptions.Timeout:
Message = f"[{self.host}] Konnektor Timeout"
except requests.exceptions.SSLError:
Message = f"[{self.host}] TLS-Verbindung fehlgeschlagen (Zertifikat?)"
except requests.exceptions.ConnectionError:
Message = f"[{self.host}] Verbindung zum Konnektor fehlgeschlagen"
except requests.exceptions.RequestException as ex:
Message = f"[{self.host}] Kommunikationsfehler: {ex}"
print(Message)
return False
def getCardHandle(self):
data = getCardHandle % (self.Mandant, self.ClientSystem, self.Arbeitsplatz)
self.headers = {
"Content-Type": 'text/xml; charset=utf-8',
"Content-Length": str(len(data)),
"SOAPAction": "http://ws.gematik.de/conn/EventService/v7.2#GetCards"
}
if not self.konComm('/service/systeminformationservice', data):
return False
dom = xml.dom.minidom.parseString(self.response.text)
node = dom.childNodes[0].childNodes[0].childNodes[0].childNodes[1]
if self.usage == 'showcards':
print(f'Type CTID ICCSN CardHandle CardHolderName')
for n in node.childNodes:
Card = {}
ICCSN = ''
for e in n.childNodes:
prefix, tag = xml.dom.minidom._nssplit(e.nodeName)
if tag == "CardType":
Card['CardType'] = e.childNodes[0].data
elif tag == "CardHandle":
Card['CardHandle'] = e.childNodes[0].data
elif tag == "CtId":
Card['CtId'] = e.childNodes[0].data
elif tag == "Iccsn":
ICCSN = e.childNodes[0].data
elif tag == "CardHolderName":
Card['CardHolderName'] = e.childNodes[0].data
if self.iccsn and self.iccsn == ICCSN:
self.Card = Card
return True
if self.usage == 'showcards':
print(f"{Card['CardType']: <6} {Card['CtId']: <10} {ICCSN: <20} {Card['CardHandle']: <36} {Card['CardHolderName']: <36}")
if self.iccsn:
print(f'[{self.host}] ICCSN {self.iccsn} nicht gefunden')
return False
return True
def pinMgmt(self):
if self.usage == 'showcards':
return True
if not self.Card:
return False
if self.Card['CardType'] == 'SMC-B':
PinType = 'PIN.SMC'
elif self.Card['CardType'] == 'HBA' and self.QES:
PinType = 'PIN.QES'
else:
PinType = 'PIN.CH'
data = pinBody % (self.usage, self.Mandant, self.ClientSystem, self.Arbeitsplatz, '1', self.Card['CardHandle'], PinType, self.usage)
self.headers = {
"Content-Type": 'text/xml; charset=utf-8',
"Content-Length": str(len(data)),
"SOAPAction": f"http://ws.gematik.de/conn/CardService/v8.1#{self.usage}"
}
if not self.konComm('/service/cardservice', data):
return False
dom = xml.dom.minidom.parseString(self.response.text)
print(f'[{self.host}]')
print(dom.toprettyxml())
return True
################# Main Function ####################
if __name__ == "__main__":
initializeDicts()
args = parseArgs()
if args.debug or Sanitizing():
threads = []
for host in ipRange(args.ipArray):
t = threading.Thread(target=main, args=(host, args))
t.start()
threads.append(t)
for t in threads:
t.join()Das Programm steht unter der BSD Lizenz.
Aufrufbeispiele
$ ./pinguino.py -i 172.18.2.1 -m 20240916_M -c 20240916_C -a 20240916_A1 -s cert ~/pinguino/20240916_C.pem
Type CTID ICCSN CardHandle CardHolderName
EGK CT_ID_0005 80276883110000115745 a1676116-70c3-4dc6-a604-cc48ed0c4087 Prof. Dr. Hillary Ulrica BäckerTEST-ONLY
SMC-B CT_ID_0005 80276883110000141437 688859f3-3d14-4cda-8d1b-d89da7700a82 Praxis Ali Freiherr HeckhausénTEST-ONLY
SMC-KT CT_ID_0005 80276883110000144725 7ba1f968-9c8b-4c49-826c-38853d89901d 80276883110000144725
HBA CT_ID_0005 80276883110000136829 ba1238f8-d816-4ddf-90e8-5ad3bb0b782d Waltraut GeröllheimerTEST-ONLY
$ ./pinguino.py -i 172.18.2.1 -m 20240916_M -c 20240916_C -a 20240916_A1 -s cert ~/pinguino/20240916_C.pem -I 80276883110000136829
<?xml version="1.0" ?>
<S:Envelope xmlns:S=http://schemas.xmlsoap.org/soap/envelope/>
<S:Body>
<ns7:GetPinStatusResponse xmlns:ns2=http://ws.gematik.de/conn/ConnectorCommon/v5.0 xmlns:ns3=http://ws.gematik.de/tel/error/v2.0 xmlns:ns4="urn:oasis:names:tc:dss:1.0:core:schema" xmlns:ns5=http://www.w3.org/2000/09/xmldsig# xmlns:ns6=http://ws.gematik.de/conn/CardServiceCommon/v2.0 xmlns:ns7=http://ws.gematik.de/conn/CardService/v8.1 xmlns:ns8=http://ws.gematik.de/conn/ConnectorContext/v2.0 xmlns:ns9=http://ws.gematik.de/int/version/ProductInformation/v1.1 xmlns:ns10="urn:oasis:names:tc:SAML:1.0:assertion">
<ns2:Status>
<ns2:Result>OK</ns2:Result>
</ns2:Status>
<ns7:PinStatus>TRANSPORT_PIN</ns7:PinStatus>
</ns7:GetPinStatusResponse>
</S:Body>
</S:Envelope>
$ ./pinguino.py -i 172.18.2.1 -m 20240916_M -c 20240916_C -a 20240916_A1 -s cert ~/pinguino/20240916_C.pem -I 80276883110000136829 -u change --qes
<?xml version="1.0" ?>
<S:Envelope xmlns:S=http://schemas.xmlsoap.org/soap/envelope/>
<S:Body>
<ns7:ChangePinResponse xmlns:ns2=http://ws.gematik.de/conn/ConnectorCommon/v5.0 xmlns:ns3=http://ws.gematik.de/tel/error/v2.0 xmlns:ns4="urn:oasis:names:tc:dss:1.0:core:schema" xmlns:ns5=http://www.w3.org/2000/09/xmldsig# xmlns:ns6=http://ws.gematik.de/conn/CardServiceCommon/v2.0 xmlns:ns7=http://ws.gematik.de/conn/CardService/v8.1 xmlns:ns8=http://ws.gematik.de/conn/ConnectorContext/v2.0 xmlns:ns9=http://ws.gematik.de/int/version/ProductInformation/v1.1 xmlns:ns10="urn:oasis:names:tc:SAML:1.0:assertion">
<ns2:Status>
<ns2:Result>Warning</ns2:Result>
<ns3:Error>
<ns3:MessageID>7b3e6df0-8dc8-4464-a4b0-30ce8fd4a248</ns3:MessageID>
<ns3:Timestamp>2025-06-07T02:17:41.334Z</ns3:Timestamp>
<ns3:Trace>
<ns3:EventID>944a68d4-a265-4fb8-8e97-fe96b5e4e929</ns3:EventID>
<ns3:Instance>Konnektor-Lokal</ns3:Instance>
<ns3:LogReference/>
<ns3:CompType>Konnektor</ns3:CompType>
<ns3:Code>4049</ns3:Code>
<ns3:Severity>Error</ns3:Severity>
<ns3:ErrorType>Technical</ns3:ErrorType>
<ns3:ErrorText>Abbruch durch den Benutzer</ns3:ErrorText>
</ns3:Trace>
</ns3:Error>
</ns2:Status>
<ns6:PinResult>ERROR</ns6:PinResult>
</ns7:ChangePinResponse>
</S:Body>
</S:Envelope>
