Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 80 additions & 23 deletions .github/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ This document describes the functionalitiy and structure of OpenHealthCardKit.
Generated API docs are available at https://gematik.github.io/ref-OpenHealthCardKit.
== Getting Started

OpenHealthCardKit requires Swift 5.1.
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.3.0"),`
`.package(url: "https://github.com/gematik/ref-OpenHealthCardKit", from: "5.6.0"),`

- **Carthage:** Put this in your `Cartfile`:

Expand Down Expand Up @@ -116,29 +116,30 @@ let eSign = EgkFileSystem.DF.ESIGN
let selectEsignCommand = HealthCardCommand.Select.selectFile(with: eSign.aid)
----

===== Setting an execution target
===== 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.

as one kind of a `HealthCardType` implementing the `CardType` protocol and then send the command to the card (or card's channel):
[source,swift]
----
// initialize your CardReaderType instance
let cardReader: CardReaderType = CardSimulationTerminalTestCase.reader
let card = try cardReader.connect([:])!
let healthCardStatus = HealthCardStatus.valid(cardType: .egk(generation: .g2))
let eGk = try HealthCard(card: card, status: healthCardStatus)
let publisher: AnyPublisher<HealthCardResponseType, Error> = selectEsignCommand.publisher(for: eGk)
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
}
----


*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+).

[source,swift]
----
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
Expand Down Expand Up @@ -219,17 +220,11 @@ Take the necessary preparatory steps for signing a challenge on the Health Card,

[source,swift]
----
expect {
let challenge = Data([0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8])
let format2Pin = try Format2Pin(pincode: "123456")
return try Self.healthCard.verify(pin: format2Pin, type: EgkFileSystem.Pin.mrpinHome)
.flatMap { _ in
Self.healthCard.sign(data: challenge)
}
.eraseToAnyPublisher()
.test()
.responseStatus
} == ResponseStatus.success
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.sign(data: challenge)
expect(signResponse.responseStatus) == ResponseStatus.success
----


Expand All @@ -238,7 +233,7 @@ steps for establishing a secure channel with the Health Card and expose only a s

[source,swift]
----
try KeyAgreement.Algorithm.idPaceEcdhGmAesCbcCmac128.negotiateSessionKey(
let secureMessaging = try await KeyAgreement.Algorithm.idPaceEcdhGmAesCbcCmac128.negotiateSessionKey(
card: CardSimulationTerminalTestCase.healthCard,
can: can,
writeTimeout: 0,
Expand All @@ -253,6 +248,68 @@ for more already implemented use cases.

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.

[source,swift]
----
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.

[source,swift]
----
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`.

[source,swift]
----
} 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

Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.DS_Store
/.build
/.build
/Packages
.vscode
.swiftpm
Expand Down
26 changes: 19 additions & 7 deletions IntegrationTests/HealthCardAccess/PublisherIntegrationTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,31 +54,43 @@ final class PublisherIntegrationTest: CardSimulationTerminalTestCase {
} == ResponseStatus.endOfFileWarning
}

// swiftlint:disable force_unwrapping
func codeForUserManual() {
func testCodeForUserManual() async throws {
// tag::createCommand[]
let eSign = EgkFileSystem.DF.ESIGN
let selectEsignCommand = HealthCardCommand.Select.selectFile(with: eSign.aid)
// end::createCommand[]

// tag::evaluateResponseStatus[]
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
}
// end::evaluateResponseStatus[]

// expect that no error has been thrown
}

// swiftlint:disable force_unwrapping
func codeForUserManual_publisher() {
let eSign = EgkFileSystem.DF.ESIGN
let selectEsignCommand = HealthCardCommand.Select.selectFile(with: eSign.aid)

expect {
// tag::setExecutionTarget[]
// initialize your CardReaderType instance
let cardReader: CardReaderType = CardSimulationTerminalTestCase.reader
let card = try cardReader.connect([:])!
let healthCardStatus = HealthCardStatus.valid(cardType: .egk(generation: .g2))
let eGk = try HealthCard(card: card, status: healthCardStatus)
let publisher: AnyPublisher<HealthCardResponseType, Error> = selectEsignCommand.publisher(for: eGk)
// end::setExecutionTarget[]

// tag::evaluateResponseStatus[]
// tag::evaluateResponseStatus_publisher[]
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
}
// end::evaluateResponseStatus[]
// end::evaluateResponseStatus_publisher[]

// tag::createCommandSequence[]
let readCertificate = checkResponse
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ final class AuthenticateChallengeE256Test: CardSimulationTerminalTestCase {
let challenge = "1234567890".data(using: .utf8)!
_ = try await Self.healthCard
.verify(pin: "123456", type: .mrpinHome)
let authenticatedResult = try await Self.healthCard.authenticate(challenge: challenge)
let authenticatedResult = try await Self.healthCard.authenticateAsync(challenge: challenge)

expect(authenticatedResult.certificate.signatureAlgorithm) == .ecdsaSha256
expect(authenticatedResult.certificate.certificate.count) == 885
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ final class AuthenticateChallengeR2048Test: CardSimulationTerminalTestCase {
let challenge = "1234567890".data(using: .utf8)!
_ = try await Self.healthCard
.verify(pin: "123456", type: .mrpinHome)
let authenticatedResult = try await Self.healthCard.authenticate(challenge: challenge)
let authenticatedResult = try await Self.healthCard.authenticateAsync(challenge: challenge)

expect(authenticatedResult.certificate.signatureAlgorithm) == .sha256RsaMgf1
expect(authenticatedResult.certificate.certificate.count) == 1242
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ final class CardChannelTypeExtVersionIntegrationTest: CardSimulationTerminalTest
}

func testReadCardTypeFromVersion() async throws {
let cardType = try await Self.healthCard.currentCardChannel.readCardType()
let cardType = try await Self.healthCard.currentCardChannel.readCardTypeAsync()
expect(cardType) == HealthCardPropertyType.egk(generation: .g2_1)
}

Expand All @@ -51,8 +51,8 @@ final class CardChannelTypeExtVersionIntegrationTest: CardSimulationTerminalTest
}

func testDetermineCardAidThenReadCardTypeFromVersion() async throws {
let cardAid = try await Self.healthCard.currentCardChannel.determineCardAid()
let cardType = try await Self.healthCard.currentCardChannel.readCardType(cardAid: cardAid)
let cardAid = try await Self.healthCard.currentCardChannel.determineCardAidAsync()
let cardType = try await Self.healthCard.currentCardChannel.readCardTypeAsync(cardAid: cardAid)
expect(cardType) == HealthCardPropertyType.egk(generation: .g2_1)
}

Expand All @@ -66,7 +66,7 @@ final class CardChannelTypeExtVersionIntegrationTest: CardSimulationTerminalTest

func testReadCardTypeFromVersionWithKnownCardAid() async throws {
let cardAid = CardAid.egk
let cardType = try await Self.healthCard.currentCardChannel.readCardType(cardAid: cardAid)
let cardType = try await Self.healthCard.currentCardChannel.readCardTypeAsync(cardAid: cardAid)
expect(cardType) == HealthCardPropertyType.egk(generation: .g2_1)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ final class DetermineCardAidIntegrationTest: CardSimulationTerminalTestCase {
}

func testDetermineCardAid() async throws {
let result = try await Self.healthCard.currentCardChannel.determineCardAid()
let result = try await Self.healthCard.currentCardChannel.determineCardAidAsync()
expect(result) == CardAid.egk
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ final class HealthCardTypeExtESIGNIntegrationTest: CardSimulationTerminalTestCas
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.sign(data: challenge)
let signResponse = try await Self.healthCard.signAsync(data: challenge)
expect(signResponse.responseStatus) == ResponseStatus.success
// end::signChallenge[]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ final class HealthCardTypeExtEfCardAccessIntTest: CardSimulationTerminalTestCase
}

func testReadEfCardAccess() async throws {
let algorithm = try await Self.healthCard.currentCardChannel.readKeyAgreementAlgorithm(
let algorithm = try await Self.healthCard.currentCardChannel.readKeyAgreementAlgorithmAsync(
writeTimeout: 30,
readTimeout: 30
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ final class HealthCardTypeExtResetRetryCounterIntegrationTest: CardSimulationTer
let puk = "12345678" as Format2Pin
let newPin = "654321" as Format2Pin

let response = try await Self.healthCard.resetRetryCounterAndSetNewPin(
let response = try await Self.healthCard.resetRetryCounterAndSetNewPinAsync(
puk: puk,
newPin: newPin,
type: EgkFileSystem.Pin.mrpinHome,
Expand Down Expand Up @@ -99,7 +99,7 @@ final class HealthCardTypeExtResetRetryCounterIntegrationTest: CardSimulationTer
let puk = "12345678" as Format2Pin
let tooLongNewPin = "654112341234" as Format2Pin

let response = try await Self.healthCard.resetRetryCounterAndSetNewPin(
let response = try await Self.healthCard.resetRetryCounterAndSetNewPinAsync(
puk: puk,
newPin: tooLongNewPin,
type: EgkFileSystem.Pin.mrpinHome,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ final class HealthCardTypeExtResetRetryCounterIntegrationTestContCont: CardSimul
// With setting a new PIN
var response: ResetRetryCounterResponse

response = try await Self.healthCard.resetRetryCounterAndSetNewPin(
response = try await Self.healthCard.resetRetryCounterAndSetNewPinAsync(
puk: wrongPuk,
newPin: newPin,
type: EgkFileSystem.Pin.mrpinHome,
dfSpecific: false
)
expect(response) == ResetRetryCounterResponse.wrongSecretWarning(retryCount: 9)

response = try await Self.healthCard.resetRetryCounterAndSetNewPin(
response = try await Self.healthCard.resetRetryCounterAndSetNewPinAsync(
puk: wrongPuk,
newPin: newPin,
type: EgkFileSystem.Pin.mrpinHome,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ final class HealthCardTypeExtVerifyPinTest: CardSimulationTerminalTestCase {
func testVerifyMrPinHomeEgk21() async throws {
let pinCode = "123456"
let format2Pin = try Format2Pin(pincode: pinCode)
let response = try await Self.healthCard.verify(pin: format2Pin, type: EgkFileSystem.Pin.mrpinHome)
let response = try await Self.healthCard.verifyAsync(pin: format2Pin, type: EgkFileSystem.Pin.mrpinHome)
expect(response) == VerifyPinResponse.success
}

Expand All @@ -56,7 +56,7 @@ final class HealthCardTypeExtVerifyPinTest: CardSimulationTerminalTestCase {
func testVerifyMrPinHomeEgk21_WarningRetryCounter() async throws {
let pinCode = "654321"
let format2Pin = try Format2Pin(pincode: pinCode)
let response = try await Self.healthCard.verify(pin: format2Pin, type: EgkFileSystem.Pin.mrpinHome)
let response = try await Self.healthCard.verifyAsync(pin: format2Pin, type: EgkFileSystem.Pin.mrpinHome)
// Note: The retry counter is not reset after each test case. Therefore, the retry counter is 1 here.
expect(response) == VerifyPinResponse.wrongSecretWarning(retryCount: 1)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ final class OpenSecureSessionIntegrationTest: CardSimulationTerminalTestCase {
func testOpenSecureSession() async throws {
let can = try! CAN.from(Data("123123".utf8)) // swiftlint:disable:this force_try

let secureHealthCard = try await Self.card.openSecureSession(can: can, writeTimeout: 0, readTimeout: 0)
let secureHealthCard = try await Self.card.openSecureSessionAsync(can: can, writeTimeout: 0, readTimeout: 0)
expect(secureHealthCard.status.type) == .egk(generation: .g2)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ final class ReadAutCertificateE256Test: CardSimulationTerminalTestCase {

func testReadAutCertificateE256() async throws {
var autCertificateResponse: AutCertificateResponse?
autCertificateResponse = try await Self.healthCard.readAutCertificate()
autCertificateResponse = try await Self.healthCard.readAutCertificateAsync()

expect(autCertificateResponse?.info) == .efAutE256
expect(autCertificateResponse?.certificate) == expectedCertificate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ final class ReadAutCertificateR2048Test: CardSimulationTerminalTestCase {
func testReadAutCertificate2048() async throws {
var autCertificateResponse: AutCertificateResponse?
autCertificateResponse = try await CardSimulationTerminalTestCase.healthCard
.readAutCertificate()
.readAutCertificateAsync()
expect(autCertificateResponse?.info) == .efAutR2048
expect(autCertificateResponse?.certificate) == expectedCertificate
}
Expand Down
Loading