OpenHealthCardKit

目录

用于访问远程信息处理基础设施的智能卡的控制/用例框架。

简介

OpenHealthCardKit 模块旨在用于参考,当实现一个系统,该系统使用 NFC、蓝牙或 USB 接口执行基于 iOS 的移动设备和德国健康卡(elektronische Gesundheitskarte)之间的通信时。

本文档描述了 OpenHealthCardKit 的功能和结构。

API 文档

生成的 API 文档可在Swift Package Index找到

注意
目前,HealthCardControl的自动 API 文档生成已损坏。 可以通过 Xcode 手动生成:选择目标 HealthCardControl 并选择 Product → Build Documentation

入门

OpenHealthCardKit 需要 Swift 5.6。

集成设置

  • Swift Package Manager: 将此放入您的 Package.swift

    .package(url: "https://github.com/gematik/ref-OpenHealthCardKit", from: "5.6.0"),
  • Carthage: 将此放入您的 Cartfile

    github "gematik/ref-openHealthCardKit" ~> 5.0

开发设置

运行 $ make setup 以开始本地开发。 这将确保所有依赖项都已就位,并且 Xcode 项目将被生成和/或覆盖。

依赖项现在是 SPM (Swift Package Manager) 和 Carthage 的混合体。 Xcode 项目是使用 xcodegen 生成的。 更复杂的构建配置是借助 Fastlane 完成的。 有关完整设置,请参见 ./fastlane 目录。

概述

OpenHealthCardKit 捆绑了子模块,这些子模块提供了通过移动 iOS 设备访问和与德国健康卡交互所需的功能。

OpenHealthCardKit 由以下子模块组成:

  • CardReaderProviderApi

  • HealthCardAccess

  • HealthCardControl

  • NFCCardReaderProvider

作为每个子模块用法的参考,另请参见 IntegrationTests

CardReaderProviderApi

(智能)卡读卡器协议,用于与 HealthCardAccess 交互。

HealthCardAccess

该库包含卡、命令、卡文件系统和错误处理的类。

HealthCardAccess API

HealthCardAccessKit API 结构包含 HealthCard 类,该类表示所有支持的卡类型,CommandsResponses 组,其中包含所有受支持的健康卡命令和响应,CardObjects 组,其中包含健康卡上可能的对象以及 Operation 组,用于级联和执行健康卡上的命令。

健康卡

HealthCard 通过存储 HealthCardStatus 属性来表示潜在的健康卡类型,如果该属性为有效,则它本身存储一个 HealthCardPropertyType,在撰写本文时,该属性由以下任一项表示

  • egk ("elektronische Gesundheitskarte")

  • hba ("Heilberufeausweis")

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

HealthCardPropertyType 本身也存储 CardGeneration(G1、G1P、G2、G2.1)。

此外,HealthCard 对象包含来自读卡器的物理卡和当前卡通道。

命令

Commands 组包含所有可通过 HealthCardCommandBuilder 获取的健康卡的可用 HealthCardCommand 对象。

代码示例

创建命令

此 API 的设计遵循 命令设计模式,并利用 Swift 的 Combine Framework。 命令对象旨在满足 Gematik COS 规范中描述的用例。 创建命令对象或序列后,您可以使用 publisher(for:) 在 Healthcard 上执行它。 有关如何配置命令的更多信息,也可以在 Gematik COS 规范中找到。

以下示例应向智能卡发送一个 SELECT 和一个 READ 命令,以便选择并读取存储在应用程序 ESIGN 中的文件 EF.C.CH.AUT.R2048 中的证书。

首先,我们要通过传递 ApplicationIdentifier 来创建一个 SelectCommand 对象。 我们通过使用 HealthCardCommand.Select 使用一个预定义的辅助函数。

也可以使用 HealthCardCommandBuilder 通过手动设置 APDU 字节来构造自定义的 HealthCardCommand

let eSign = EgkFileSystem.DF.ESIGN
let selectEsignCommand = HealthCardCommand.Select.selectFile(with: eSign.aid)
命令执行

我们执行已创建的命令 CardType 实例,该实例通常由 CardReaderType 提供。

在下一个示例中,我们使用一个 HealthCard 对象,该对象代表 eGK(elektronische Gesundheitskarte),作为实现 CardType 协议的 HealthCardType 的一种,然后将命令发送到卡(或卡的通道)

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

以下段落描述了通过 Combine 接口执行命令的已弃用方式

可以使用 publisher(for:writetimeout:readtimeout) 将创建的命令提升到 Combine 框架。 可以针对预期的 ResponseStatus(例如,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
}
创建命令序列

可以通过 flatMap 运算符链接更多命令以进行后续执行:首先创建一个命令并将其提升到 Combine monad 上,然后使用 flatMap 运算符创建一个发布者,例如

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

最终使用 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()
处理执行结果

当整个命令链设置完毕后,我们必须订阅它。 我们实际上只会收到一个完成前的值,因此像这个 sink() 便捷发布者这样的东西非常有用。

_ = 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

可以使用此库来实现通过移动设备与德国健康卡(eGk、elektronische Gesundheitskarte)交互的用例。

通常,您会将此库用作移动应用程序的高级 API 网关,以将预定义的命令链发送到健康卡并解释响应。

有关更多信息,请找到低级部分 HealthCardAccess 和 GitHub 上的 Demo App

请参阅 Gematik GitHub IO 页面以获取更全面的概述。

代码示例

采取必要的准备步骤,以便在健康卡上签名一个挑战,然后对其进行签名。

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

封装 PACE 协议步骤,以建立与健康卡的安全通道,并仅公开一个简单的 API 调用。

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

有关更多已实现的用例,请参见集成测试 IntegrationTests/HealthCardControl/。

NFCCardReaderProvider

CardReaderProvider 实现,用于处理与 Apple iPhone NFC 接口的通信。

NFCCardReaderSession

为了方便起见,NFCCardReaderSession 将 NFC 接口的使用与 HealthCardAccess/HealthCardControl 层结合在一起。

初始化程序接受一些 NFC 显示消息、CAN(卡访问号码)和一个包含 NFCHealthCardSessionHandle 的闭包,以将命令/响应发送/接收到/从 NFC 健康卡,并更新用户的界面消息。

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 NFCSigningFunctionController.Error.wrongPin(retryCount: count)
    } else if case VerifyPinResponse.passwordBlocked = verifyPinResponse {
        throw NFCSigningFunctionController.Error.passwordBlocked
    } else if VerifyPinResponse.success != verifyPinResponse {
        throw NFCSigningFunctionController.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

在 NFC 健康卡上执行操作。 在执行操作之前,会首先建立安全通道 (PACE)。

signedData = try await nfcHealthCardSession.executeOperation()

抛出的错误类型为 NFCHealthCardSessionErrorNFCHealthCardSession 还为您提供了一个端点来使底层 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

NFCDemo iOS App 目标通过利用上述框架连接到 eGK 卡并通过 NFC 建立安全通信通道来演示 OHCKit 和 NFCCardReader[Provider] 的使用。

该应用程序由两个屏幕/视图组成。 第一个将提示用户输入 CAN 号码。 第二个提示输入 PIN。 当点击 connect 按钮时,将在卡上针对 mrpinHome 验证此 PIN。

许可证

版权所有 2023 gematik GmbH

根据 Apache License, Version 2.0(“许可证”)获得许可;除非遵守许可证,否则您不得使用此文件。

有关管理权限和限制的特定语言,请参见 许可证

除非适用法律要求,否则该软件按“原样”提供,不提供任何形式的明示或暗示的担保,包括但不限于对特定用途的适用性、适销性和/或不侵权的担保。 作者或版权所有者在任何情况下均不对因软件或软件的使用或其他交易而引起或与之相关的任何损害或其他索赔承担任何责任,无论是在合同诉讼、侵权诉讼还是其他诉讼中。

该软件是研发活动的成果,因此不一定经过质量保证,并且不具有应承担责任的产品特性。 因此,gematik 不提供任何支持或其他用户帮助(除非在个别情况下另有说明,且没有法律义务的正当理由)。 此外,不要求进一步开发和调整结果以适应最新的技术水平。

Gematik 可以在任何时候从发布地点临时或永久删除已发布的结果,而无需事先通知或说明理由。