Skip to content

Working with NFC

Boriss Melikjan edited this page Apr 6, 2026 · 1 revision

Aside from signing and decrypting documents with ID-card, you can use IdCardLib to read data from ID-card using NFC antennae in iOS device.

You don't need to worry about handling connections with NFC antennae. IdCardLib will do that for you. Whenever you call a method, that needs interaction with NFC antennae, IdCardLib will attempt to create a tunnel and connect to NFC chip on ID card. When no ID card with NFC chip is present in device vicinity, it will ask user to hold card near the iOS device antennae.

Supported NFC tags

NFCISO7816Tag - https://developer.apple.com/documentation/corenfc/nfciso7816tag

Creating NFC PACE tunnel and performing card command

// Allow CoreNFC types to cross async boundaries in this controlled context
extension NFCTagReaderSession: @unchecked @retroactive Sendable {}
extension NFCTag: @unchecked @retroactive Sendable {}

@MainActor
public class OperationReadCertificate: NSObject {
    private var session: NFCTagReaderSession?
    private var CAN: String = ""
    private var certUsage: CertificateUsage!
    private let nfcMessage: String = "Please place your ID card against the smart device"
    private let connection = NFCConnection()
    private var continuation: CheckedContinuation<SecCertificate, Error>?

    public func startReading(CAN: String, certUsage: CertificateUsage) async throws -> SecCertificate {

        return try await withCheckedThrowingContinuation { continuation in
            self.continuation = continuation

            guard NFCTagReaderSession.readingAvailable else {
                continuation.resume(throwing: IdCardInternalError.nfcNotSupported)
                return
            }

            self.CAN = CAN
            self.certUsage = certUsage
            session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self)
            session?.alertMessage = nfcMessage
            session?.begin()
        }
    }
}

extension OperationReadCertificate: NFCTagReaderSessionDelegate {
    public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
        Task { @MainActor in
            defer {
                self.session = nil
            }

            guard let certUsage else {
                continuation?.resume(throwing: ReadCertificateError.certificateUsageNotSpecified)
                session.invalidate(errorMessage: "Failed to read data")
                return
            }
            do {
                session.alertMessage = "Hold your ID card against your smart device until the data is read"
                let tag = try await connection.setup(session, tags: tags)
                let cardCommands = try await connection.getCardCommands(session, tag: tag, CAN: CAN)
                do {
                    switch certUsage {
                    case .auth:
                        let cert = try await cardCommands.readAuthenticationCertificate()
                        let x509Certificate = try convertBytesToX509Certificate(cert)
                        continuation?.resume(with: .success(x509Certificate))
                    case .sign:
                        let cert = try await cardCommands.readSignatureCertificate()
                        let x509Certificate = try convertBytesToX509Certificate(cert)
                        continuation?.resume(with: .success(x509Certificate))
                    }
                    session.alertMessage = "Data read"
                    session.invalidate()
                } catch {
                    session.invalidate(errorMessage: "Failed to read data")
                    continuation?.resume(throwing: ReadCertificateError.failedToReadCertificate)
                }
            } catch {
                session.invalidate(errorMessage: "Failed to read data")
                continuation?.resume(throwing: error)
            }
        }
    }

    public func tagReaderSessionDidBecomeActive(_: NFCTagReaderSession) { }

    public func tagReaderSession(_: NFCTagReaderSession, didInvalidateWithError _: Error) {
        Task { @MainActor in
            self.session = nil
        }
    }
}

Reading public data on card

You can get some data from ID card, that is publicly available. This includes things like card owner name, birth date, document number etc.

let tag = try await connection.setup(session, tags: tags)
let cardCommands = try await connection.getCardCommands(session, tag: tag, CAN: CAN)
let cardInfo = try await cardCommands.readPublicData()

Reading data from card can be time-consuming. If you don't need all available data, then you can choose to get only the set of most important values.

func readPublicData() async throws -> CardInfo
public struct CardInfo: Sendable, Hashable {
    public var givenName: String
    public var surname: String
    public var personalCode: String
    public var citizenship: String
    public var documentNumber: String
    public var dateOfExpiry: String
}

Certificates data

There are two certificates on ID card - authentication and signing certificate. You can get some basic data like expiry dates from them.

For signing certificate use

func readSignatureCertificate() async throws -> Data
let cert = try await cardCommands.readSignatureCertificate()
let x509Certificate = try convertBytesToX509Certificate(cert)
continuation?.resume(with: .success(x509Certificate))

For authentication certificate use

func readAuthenticationCertificate() async throws -> Data
let cert = try await cardCommands.readAuthenticationCertificate()
let x509Certificate = try convertBytesToX509Certificate(cert)
continuation?.resume(with: .success(x509Certificate))

PIN retry counters

PIN and PUK codes can get blocked when invalid code is used for some actions. It is important to notify user in that case, so they would know when they have to take action in order to unblock blocked code. It is not possible to continue to use certain functionality, like document signing, when PIN is blocked.

IdCardLib provides methods for getting retry counter value from ID card. The number returned represents how many retries user has left for each code. When count reaches 0, then that particular code is blocked and can't be used anymore until user unblocks it. Unblock actions are not available for third-party applications. User will have to use one of RIA DigiDoc applications to unblock.

let pin1Response = try await cardCommands.readCodeTryCounterRecord(.pin1)
OperationReadCardData.logger().info("PIN1 retry count: \(pin1Response.retryCount)")
OperationReadCardData.logger().info("PIN1 active: \(pin1Response.pinActive)")
let pin2Response = try await cardCommands.readCodeTryCounterRecord(.pin2)
OperationReadCardData.logger().info("PIN2 retry count: \(pin2Response.retryCount)")
OperationReadCardData.logger().info("PIN2 active: \(pin2Response.pinActive)")
let pukResponse = try await cardCommands.readCodeTryCounterRecord(.puk)
OperationReadCardData.logger().info("PUK retry count: \(pukResponse.retryCount)")
OperationReadCardData.logger().info("PUK active: \(pukResponse.pinActive)")

let pinResponse = PinResponse(
    pin1RetryCount: pin1Response.retryCount,
    pin1Active: pin1Response.pinActive,
    pin2RetryCount: pin2Response.retryCount,
    pin2Active: pin2Response.pinActive,
    pukRetryCount: pukResponse.retryCount,
    pukActive: pukResponse.pinActive,
)

Changing PIN1, PIN2, PUK with old code:

let tag = try await self.connection.setup(session, tags: tags)
let cardCommands = try await self.connection.getCardCommands(session, tag: tag, CAN: self.canNumber)
try await cardCommands.changeCode(codeType, to: newPin, verifyCode: currentPin)

Unblocking PIN1 and PIN2 with PUK code:

let tag = try await self.connection.setup(session, tags: tags)
let cardCommands = try await self.connection.getCardCommands(session, tag: tag, CAN: self.canNumber)
try await cardCommands.unblockCode(codeType, puk: puk, newCode: newPin)

Clone this wiki locally