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) und mit jedem Konnektor Hersteller möglich
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 requests, xml.dom.minidom, ssl, sys, re, argparse, urllib3, threading
from socket import timeout
from datetime import datetime
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)
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", "^((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 öÖäÄüÜß @#_-§%+:=.", "^[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 öÖäÄüÜß @#_-§%+:=.", "^[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 öÖäÄüÜß @#_-§%+:=.", "^[a-zA-Z0-9öÖäÄüÜß@#_\-§%+:=\.]{1,64}$"],
"authMethod": ["", "^(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 ipRange in ipRanges:
if re.search('-', ipRange):
fromIP, toOctet = ipRange.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 += [ipBase + "." + str(i) for i in range(int(fromOctet), int(toOctet) + 1, 1)]
else:
ipArray.append(ipRange)
return ipArray
def Sanitizing():
for inputVar in vars(args):
if not (inputValue := getattr(args, inputVar)):
continue
elif not type(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 not len(inputValue) == 3:
print("Bitte Benutzername und Kennwort als Parameter eingeben!")
return False
elif inputValue[0] == "cert" and not 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.keys():
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 = True if QES else False
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":
self.Auth = f'{authMethod[1]}:{authMethod[2]}'
self.Auth = "'Basic ' + str(b64encode(self.Auth.encode()))"
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})
self.response =\
self.session.post(f'{self.URL}{Service}', timeout=30, headers=self.headers, verify=False, cert=self.cert, data=data) if data else\
self.session.get(f'{self.URL}{Service}', timeout=30, headers=self.headers, verify=False, cert=self.cert)
assert self.response.status_code == 200
return True
except requests.exceptions.Timeout:
Message = "Konnektor Timeout"
except IOError:
Message = "Es ist ein Kommunikationsfehler mit dem Konnektor aufgetreten. Fehlendes Zertifikat oder TLS Verbindung fehlgeschlagen."
except AssertionError:
status_code = str(self.response.status_code)
Message = f' HTTP Status Code: {status_code}'
except:
Message = "Ein undefinierter Fehler ist aufgetreten."
if Message: 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 = {}
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}")
return True
def pinMgmt(self):
if self.usage == 'showcards': return True
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}"
}
self.konComm('/service/cardservice', data)
dom = xml.dom.minidom.parseString(self.response.text)
print(dom.toprettyxml())
return True
################# Main Function ####################
if __name__ == "__main__":
initializeDicts()
args = parseArgs()
if args.debug or Sanitizing():
for host in ipRange(args.ipArray):
threading.Thread(target=main, args=(host, args)).start()
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>