TI: SMC-B Pin oder eHBA Pin ändern ohne Primärsystem

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>