/ref-OpenHealthCardKit

Controlling/Use-case framework for accessing smart cards of the telematic infrastructure. API Documentation: https://gematik.github.io/ref-OpenHealthCardKit

Primary LanguageSwiftApache License 2.0Apache-2.0

OpenHealthCardKit

Controlling/Use-case framework for accessing smart cards of the telematic infrastructure.

Introduction

The OpenHealthCardKit module is intended for reference purposes when implementing a system that performs the communication between an iOS based mobile device and a German Health Card (elektronische Gesundheitskarte) using an NFC, Blue Tooth oder USB interface.

This document describes the functionalitiy and structure of OpenHealthCardKit.

API Documentation

Generated API docs are available at Swift Package Index:

Note
As of now the automatic API doc generation for HealthCardControl is broken. It’s possible to generate it manually via Xcode: Select the target HealthCardControl and select Product → Build Documentation.

Getting Started

OpenHealthCardKit requires Swift 5.6.

Setup for integration

  • Swift Package Manager: Put this in your Package.swift:

    .package(url: "https://github.com/gematik/ref-OpenHealthCardKit", from: "5.6.0"),
  • Carthage: Put this in your Cartfile:

    github "gematik/ref-openHealthCardKit" ~> 5.0

Setup for development

Run $ make setup to start developing locally. This will make sure all the dependencies are put in place and the Xcode-project will be generated and/or overwritten.

Dependencies are a mix of SPM (Swift Package Manager) and Carthage right now. The Xcode-project is generated using xcodegen. The more complex build configuration(s) is done with the help of Fastlane. See the ./fastlane directory for full setup.

Overview

OpenHealthCardKit bundles submodules that provide the functionality necessary for accessing and interacting with German Health Cards via a mobile iOS device.

OpenHealthCardKit consists of the submodules

  • CardReaderProviderApi

  • HealthCardAccess

  • HealthCardControl

  • NFCCardReaderProvider

As a reference for the usage of each submodule see also the IntegrationTests.

CardReaderProviderApi

(Smart)CardReader protocols for interacting with HealthCardAccess.

HealthCardAccess

This library contains the classes for cards, commands, card file systems and error handling.

HealthCardAccess API

The HealthCardAccessKit API Structure contains the HealthCard class representing all supported card types, the Commands and Responses groups with all supported commands and responses for health cards, the CardObjects group with the possible objects on a health cards and the Operation group for cascading and executing commands on health cards.

Health Cards

The class HealthCard represents the potential types of health cards by storing a HealthCardStatus property which in case of being valid by itself stores a HealthCardPropertyType which at the time of writing is represented by either one of the following

  • egk ("elektronische Gesundheitskarte")

  • hba ("Heilberufeausweis")

  • smcb ("Security Module Card Typ B").

The HealthCardPropertyType by itself stores the CardGeneration (G1, G1P, G2, G2.1) as well.

Furthermore the HealthCard object contains the physical card from a card reader and the current card channel.

Commands

The Commands groups contains all available HealthCardCommand objects for health cards through the HealthCardCommandBuilder.

Code Samples

Create a command

The design of this API follows the command design pattern leveraging Swift’s Combine Framework. The command objects are designed to fulfil the use-cases described in the Gematik COS specification. After creating a command object resp. sequence you can execute it on a Healthcard with the help of publisher(for:). More information on how to configure the commands can also be found in the Gematik COS specification.

Following example shall send a SELECT and a READ command to a smart card in order to select and read the certificate stored in the file EF.C.CH.AUT.R2048 in the application ESIGN.

First we want to to create a SelectCommand object passing a ApplicationIdentifier. We use one of the predefined helper functions by using HealthCardCommand.Select.

One could also use the HealthCardCommandBuilder to construct a customized HealthCardCommand by setting the APDU-bytes manually.

let eSign = EgkFileSystem.DF.ESIGN
let selectEsignCommand = HealthCardCommand.Select.selectFile(with: eSign.aid)
Command execution

We execute the created command CardType instance which has been typically provided by a CardReaderType.

In the next example we use a HealthCard object representing an eGK (elektronische Gesundheitskarte) as one kind of a HealthCardType implementing the CardType protocol and then send the command to the card (or card’s channel):

let healthCardResponse = try await selectEsignCommand.transmitAsync(to: Self.healthCard)
guard healthCardResponse.responseStatus == ResponseStatus.success else {
    throw HealthCard.Error.operational // TO-DO: handle this or throw a meaningful Error
}

Following paragraphs describe the deprecated way of executung commands via the Combine inteface:

A created command can be lifted to the Combine framework with publisher(for:writetimeout:readtimeout). The result of the command execution can be validated against an expected ResponseStatus, e.g. SUCCESS (0x9000).

let publisher: AnyPublisher<HealthCardResponseType, Error> = selectEsignCommand.publisher(for: eGk)
let checkResponse = publisher.tryMap { healthCardResponse -> HealthCardResponseType in
    guard healthCardResponse.responseStatus == ResponseStatus.success else {
        throw HealthCard.Error.operational // throw a meaningful Error
    }
    return healthCardResponse
}
Create a Command Sequence

It is possible to chain further commands via the flatMap operator for subsequent execution: First create a command and lift it onto a Combine monad, then create a publisher using the flatMap operator, e.g.

Just(AnyHealthCardCommand.build())
    .flatMap { command in command.pusblisher(for: card) }

Eventually use eraseToAnyPublisher().

let readCertificate = checkResponse
    .tryMap { _ -> HealthCardCommandType in
        let sfi = EgkFileSystem.EF.esignCChAutR2048.sfid!
        return try HealthCardCommand.Read.readFileCommand(with: sfi, ne: 0x076C - 1)
    }
    .flatMap { command in
        command.publisher(for: eGk)
    }
    .eraseToAnyPublisher()
Process Execution result

When the whole command chain is set up we have to subscribe to it. We really only will receive one value before completion, so something as simple as this sink() convenience publisher is useful.

_ = readCertificate
    .sink(
        receiveCompletion: { completion in
            switch completion {
            case .finished:
                Logger.integrationTest.debug("Completed")
            case let .failure(error):
                Logger.integrationTest.debug("Error: \(error)")
            }
        },
        receiveValue: { healthCardResponse in
            Logger.integrationTest.debug("Got a certifcate")
            let certificate = healthCardResponse.data!
            Logger.integrationTest.debug("Certificate: \(certificate.hexString())")
            // proceed with certificate data here
            // use swiftUI to a show success message on screen etc.
        }
    )

HealthCardControl

This library can be used to realize use cases for interacting with a German Health Card (eGk, elektronische Gesundheitskarte) via a mobile device.

Typically you would use this library as the high level API gateway for your mobile application to send predefined command chains to the Health Card and interpret the responses.

For more info, please find the low level part HealthCardAccess. and a Demo App on GitHub.

See the Gematik GitHub IO page for a more general overview.

Code Samples

Take the necessary preparatory steps for signing a challenge on the Health Card, then sign it.

let challenge = Data([0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8])
let format2Pin = try Format2Pin(pincode: "123456")
_ = try await Self.healthCard.verify(pin: format2Pin, type: EgkFileSystem.Pin.mrpinHome)
let signResponse = try await Self.healthCard.signAsync(data: challenge)
expect(signResponse.responseStatus) == ResponseStatus.success

Encapsulate the PACE protocol steps for establishing a secure channel with the Health Card and expose only a simple API call .

let secureMessaging = try await KeyAgreement.Algorithm.idPaceEcdhGmAesCbcCmac128.negotiateSessionKeyAsync(
    card: CardSimulationTerminalTestCase.healthCard,
    can: can,
    writeTimeout: 0,
    readTimeout: 10
)

See the integration tests IntegrationTests/HealthCardControl/ for more already implemented use cases.

NFCCardReaderProvider

A CardReaderProvider implementation that handles the communication with the Apple iPhone NFC interface.

NFCCardReaderSession

For convience, the NFCCardReaderSession combines the usage of the NFC inteface with the HealthCardAccess/HealthCardControl layers.

The initializer takes some NFC-Display messages, the CAN (card access number) and a closure with a NFCHealthCardSessionHandle to send/receive commands/responses to/from the NFC HealthCard and to update the user’s interface message to.

guard let nfcHealthCardSession = NFCHealthCardSession(messages: messages, can: can, operation: { session in
    session.updateAlert(message: NSLocalizedString("nfc_txt_msg_verify_pin", comment: ""))
    let verifyPinResponse = try await session.card.verifyAsync(
        pin: format2Pin,
        type: EgkFileSystem.Pin.mrpinHome
    )
    if case let VerifyPinResponse.wrongSecretWarning(retryCount: count) = verifyPinResponse {
        throw NFCLoginController.Error.wrongPin(retryCount: count)
    } else if case VerifyPinResponse.passwordBlocked = verifyPinResponse {
        throw NFCLoginController.Error.passwordBlocked
    } else if VerifyPinResponse.success != verifyPinResponse {
        throw NFCLoginController.Error.verifyPinResponse
    }

    session.updateAlert(message: NSLocalizedString("nfc_txt_msg_signing", comment: ""))
    let outcome = try await session.card.sign(
        payload: "ABC".data(using: .utf8)!, // swiftlint:disable:this force_unwrapping
        checkAlgorithm: checkBrainpoolAlgorithm
    )

    session.updateAlert(message: NSLocalizedString("nfc_txt_msg_success", comment: ""))
    return outcome
})
else {
    // handle the case the Session could not be initialized

Execute the operation on the NFC HealthCard. The secure channel (PACE) is established initially before executing the operation.

signedData = try await nfcHealthCardSession.executeOperation()

The thrown error will be of type NFCHealthCardSessionError. The NFCHealthCardSession also gives you an endpoint to invalidate the underlying TagReaderSession.

} catch NFCHealthCardSessionError.coreNFC(.userCanceled) {
    // error type is always `NFCHealthCardSessionError`
    // here we especially handle when the user canceled the session
    Task { @MainActor in self.pState = .idle } // Do some view-property update
    // Calling .invalidateSession() is not strictly necessary
    //  since nfcHealthCardSession does it while it's de-initializing.
    nfcHealthCardSession.invalidateSession(with: nil)
    return
} catch {
    Task { @MainActor in self.pState = .error(error) }
    nfcHealthCardSession.invalidateSession(with: error.localizedDescription)
    return
}

NFCDemo

The NFCDemo iOS App target demonstrates the use of OHCKit and the NFCCardReader[Provider] specifically by utilizing said framework to connect to and establish a secure communications channel with an eGK Card via NFC.

The App consist out of two screens/views. The first one will prompt the user for the CAN number. The second prompts for the PIN. This PIN is verified on the card against mrpinHome when the connect button is tapped.

License

Copyright 2023 gematik GmbH

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.

See the LICENSE for the specific language governing permissions and limitations under the License.

Unless required by applicable law the software is provided "as is" without warranty of any kind, either express or implied, including, but not limited to, the warranties of fitness for a particular purpose, merchantability, and/or non-infringement. The authors or copyright holders shall not be liable in any manner whatsoever for any damages or other claims arising from, out of or in connection with the software or the use or other dealings with the software, whether in an action of contract, tort, or otherwise.

The software is the result of research and development activities, therefore not necessarily quality assured and without the character of a liable product. For this reason, gematik does not provide any support or other user assistance (unless otherwise stated in individual cases and without justification of a legal obligation). Furthermore, there is no claim to further development and adaptation of the results to a more current state of the art.

Gematik may remove published results temporarily or permanently from the place of publication at any time without prior notice or justification.