Skip to content

Commit

Permalink
Improve NFC mutual authentication
Browse files Browse the repository at this point in the history
MOPPIOS-1421

Signed-off-by: Raul Metsma <raul@metsma.ee>
  • Loading branch information
metsma committed Aug 2, 2024
1 parent 30bcef9 commit f4133c6
Showing 1 changed file with 128 additions and 59 deletions.
187 changes: 128 additions & 59 deletions MoppApp/MoppApp/NFCSignature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,47 @@ import SwiftECC
import BigInt
import SkSigningLib

struct RuntimeError: Error {
let msg: String
}

struct PinError: Error {
let attemptsLeft: UInt8
enum NFCError: Error {
case error(msg: String)
case pinError(attemptsLeft: UInt8)
}

class NFCSignature : NSObject, NFCTagReaderSessionDelegate {

enum PasswordType: UInt8 {
case id_PasswordType_MRZ = 1 // 0.4.0.127.0.7.2.2.12.1
case id_PasswordType_CAN = 2 // 0.4.0.127.0.7.2.2.12.2
var data: String {
return switch self {
case .id_PasswordType_MRZ: "04007F000702020C01"
case .id_PasswordType_CAN: "04007F000702020C02"
}
}
}
enum MappingType: String {
case id_PACE_ECDH_GM_AES_CBC_CMAC_256 = "04007f00070202040204" // 0.4.0.127.0.7.2.2.4.2.4
var data: Data { return Data(hex: rawValue)! }
}
enum ParameterId: UInt8 {
case EC256r1 = 12
case BP256r1 = 13
case EC384r1 = 15
case BP384r1 = 16
case BP512r1 = 17
case EC521r1 = 18
var domain: Domain {
return switch self {
case .EC256r1: .instance(curve: .EC256r1)
case .EC384r1: .instance(curve: .EC384r1)
case .EC521r1: .instance(curve: .EC521r1)
case .BP256r1: .instance(curve: .BP256r1)
case .BP384r1: .instance(curve: .BP384r1)
case .BP512r1: .instance(curve: .BP512r1)
}
}
}
typealias TLV = TKBERTLVRecord

static let shared: NFCSignature = NFCSignature()
private static let dateFormatter = {
let df = DateFormatter()
Expand Down Expand Up @@ -179,15 +210,15 @@ class NFCSignature : NSObject, NFCTagReaderSessionDelegate {
ErrorUtil.generateError(signingError: .empty, details: details)
}
})
} catch let error as PinError {
printLog("\nRIA.NFC - PinError count \(error.attemptsLeft)")
switch error.attemptsLeft {
} catch NFCError.pinError(let attemptsLeft) {
printLog("\nRIA.NFC - PinError count \(attemptsLeft)")
switch attemptsLeft {
case 0: setSessionMessage(L(.pin2BlockedAlert), invalidate: true)
case 1: setSessionMessage(L(.wrongPin2Single), invalidate: true)
default: setSessionMessage(L(.wrongPin2, [error.attemptsLeft]), invalidate: true)
default: setSessionMessage(L(.wrongPin2, [attemptsLeft]), invalidate: true)
}
} catch let error as RuntimeError {
printLog("\nRIA.NFC - RuntimeError \(error.msg)")
} catch NFCError.error(let msg) {
printLog("\nRIA.NFC - error \(msg)")
setSessionMessage(L(.nfcSignFailed), invalidate: true)
} catch {
printLog("\nRIA.NFC - Error \(error.localizedDescription)")
Expand All @@ -211,16 +242,39 @@ class NFCSignature : NSObject, NFCTagReaderSessionDelegate {

// MARK: - PACE
func mutualAuthenticate(tag: NFCISO7816Tag) async throws -> (Bytes, Bytes)? {
let oid = "04007f00070202040204" // id-PACE-ECDH-GM-AES-CBC-CMAC-256
// + CAN
_ = try await tag.sendCommand(cls: 0x00, ins: 0x22, p1: 0xc1, p2: 0xa4, data: Data(hex: "800a\(oid)830102")!)
printLog("Select CardAccess")
_ = try await tag.sendCommand(cls: 0x00, ins: 0xA4, p1: 0x02, p2: 0x0C, data: Data([0x01, 0x1C]))
printLog("Read CardAccess")
let data = try await tag.sendCommand(cls: 0x00, ins: 0xB0, p1: 0x00, p2: 0x00, le: 256)

guard let (mappingType, domain) = TLV.sequenceOfRecords(from: data)?
.flatMap({ cardAccess in TLV.sequenceOfRecords(from: cardAccess.value) ?? [] })
.compactMap({ tlv in
if let records = TLV.sequenceOfRecords(from: tlv.value),
records.count == 3,
let mapping = MappingType(rawValue: records[0].value.toHex),
let parameterId = ParameterId(rawValue: records[2].value[0]) {
return (mapping, parameterId.domain)
}
return nil
})
.first else {
printLog("Unsupported mapping")
return nil
}

_ = try await tag.sendCommand(cls: 0x00, ins: 0x22, p1: 0xc1, p2: 0xa4, records: [
TLV(tag: 0x80, value: mappingType.data),
TLV(tag: 0x83, value: Data([PasswordType.id_PasswordType_CAN.rawValue]))
])

// Step1 - General Authentication
let nonceEnc = try await tag.sendPaceCommand(records: [], tagExpected: 0x80)
printLog("Challenge \(nonceEnc.toHex)")
let nonce = try decryptNonce(encryptedNonce: nonceEnc)
printLog("Nonce \(nonce.toHex)")
let domain = Domain.instance(curve: .EC256r1)

// Mapping data
// Step2
let (terminalPubKey, terminalPrivKey) = domain.makeKeyPair()
let mappingKey = try await tag.sendPaceCommand(records: [try TKBERTLVRecord(tag: 0x81, publicKey: terminalPubKey)], tagExpected: 0x82)
printLog("Mapping key \(mappingKey.toHex)")
Expand All @@ -233,12 +287,12 @@ class NFCSignature : NSObject, NFCTagReaderSessionDelegate {
let sharedSecretH = try domain.multiplyPoint(cardPubKey.w, terminalPrivKey.s)
printLog("Shared Secret x: \(sharedSecretH.x.asMagnitudeBytes().toHex), y: \(sharedSecretH.y.asMagnitudeBytes().toHex)")
let mappedPoint = try domain.addPoints(mappingBasePoint.w, sharedSecretH) // MAP G = (S*G) + H
printLog("Mapped point x: \(mappedPoint.x.asMagnitudeBytes().toHex), y: \(mappedPoint.y.asMagnitudeBytes().toHex)")
let mappedDomain = try Domain.instance(name: domain.name + " Mapped", p: domain.p, a: domain.a, b: domain.b, gx: mappedPoint.x, gy: mappedPoint.y, order: domain.order, cofactor: domain.cofactor)

// Ephemeral data
printLog("Mapped point x: \(mappedPoint.x.asMagnitudeBytes().toHex), y: \(mappedPoint.y.asMagnitudeBytes().toHex)")
let mappedDomain = try Domain.instance(name: domain.name + " Mapped", p: domain.p, a: domain.a, b: domain.b, gx: mappedPoint.x, gy: mappedPoint.y, order: domain.order, cofactor: domain.cofactor)
let (terminalEphemeralPubKey, terminalEphemeralPrivKey) = mappedDomain.makeKeyPair()
let ephemeralKey = try await tag.sendPaceCommand(records: [try TKBERTLVRecord(tag: 0x83, publicKey: terminalEphemeralPubKey)], tagExpected: 0x84)
let ephemeralKey = try await tag.sendPaceCommand(records: [try TLV(tag: 0x83, publicKey: terminalEphemeralPubKey)], tagExpected: 0x84)
printLog("Card Ephermal key \(ephemeralKey.toHex)")
let ephemeralCardPubKey = try ECPublicKey(domain: mappedDomain, point: ephemeralKey)!

Expand All @@ -251,15 +305,21 @@ class NFCSignature : NSObject, NFCTagReaderSessionDelegate {
printLog("KS.Mac \(ksMac.toHex)")

// Mutual authentication
let macHeader = Bytes(hex: "7f494f060a\(oid)8641")!
let macCalc = try AES.CMAC(key: ksMac)
let ephemeralCardPubKeyBytes = try ephemeralCardPubKey.x963Representation()
let macValue = try await tag.sendPaceCommand(records: [TKBERTLVRecord(tag: 0x85, bytes: (try macCalc.authenticate(bytes: macHeader + ephemeralCardPubKeyBytes)))], tagExpected: 0x86)

let macHeader = TLV(tag: 0x7f49, records: [
TLV(tag: 0x06, value: mappingType.data),
TLV(tag: 0x86, bytes: try ephemeralCardPubKey.x963Representation())
])
let macValue = try await tag.sendPaceCommand(records: [TLV(tag: 0x85, bytes: (try macCalc.authenticate(bytes: macHeader.data)))], tagExpected: 0x86)
printLog("Mac response \(macValue.toHex)")

// verify chip's MAC and return session keys
let terminalEphemeralPubKeyBytes = try terminalEphemeralPubKey.x963Representation()
if macValue == Data(try macCalc.authenticate(bytes: macHeader + terminalEphemeralPubKeyBytes)) {
let macResult = TLV(tag: 0x7f49, records: [
TLV(tag: 0x06, value: mappingType.data),
TLV(tag: 0x86, bytes: try terminalEphemeralPubKey.x963Representation())
])
if macValue == Data(try macCalc.authenticate(bytes: macResult.data)) {
return (ksEnc, ksMac)
}
return nil
Expand All @@ -274,29 +334,29 @@ class NFCSignature : NSObject, NFCTagReaderSessionDelegate {
if !data.isEmpty {
let iv = try AES.CBC(key: ksEnc!).encrypt(SSC!)
let enc_data = try AES.CBC(key: ksEnc!, iv: iv).encrypt(data.addPadding())
DO87 = TKBERTLVRecord(tag: 0x87, bytes: [0x01] + enc_data).data
DO87 = TLV(tag: 0x87, bytes: [0x01] + enc_data).data
} else {
DO87 = Data()
}
let DO97: Data
if le > 0 {
DO97 = TKBERTLVRecord(tag: 0x97, bytes: [UInt8(le == 256 ? 0 : le)]).data
DO97 = TLV(tag: 0x97, bytes: [UInt8(le == 256 ? 0 : le)]).data
} else {
DO97 = Data()
}
let cmd_header: Bytes = [cls | 0x0C, ins, p1, p2]
let M = cmd_header.addPadding() + DO87 + DO97
let N = SSC! + M
let mac = try AES.CMAC(key: ksMac!).authenticate(bytes: N.addPadding())
let DO8E = TKBERTLVRecord(tag: 0x8E, bytes: mac).data
let DO8E = TLV(tag: 0x8E, bytes: mac).data
let send = DO87 + DO97 + DO8E
printLog(">: \(send.toHex)")
let response = try await tag.sendCommand(cls: cmd_header[0], ins: ins, p1: p1, p2: p2, data: send, le: 256)
printLog("<: \(response.toHex)")
var tlvEnc: TKTLVRecord?
var tlvRes: TKTLVRecord?
var tlvMac: TKTLVRecord?
for tlv in TKBERTLVRecord.sequenceOfRecords(from: response)! {
for tlv in TLV.sequenceOfRecords(from: response) ?? [] {
switch tlv.tag {
case 0x87: tlvEnc = tlv
case 0x99: tlvRes = tlv
Expand All @@ -305,17 +365,17 @@ class NFCSignature : NSObject, NFCTagReaderSessionDelegate {
}
}
guard tlvRes != nil else {
throw RuntimeError(msg: "Missing RES tag")
throw NFCError.error(msg: "Missing RES tag")
}
guard tlvMac != nil else {
throw RuntimeError(msg: "Missing MAC tag")
throw NFCError.error(msg: "Missing MAC tag")
}
let K = SSC!.increment() + (tlvEnc?.data ?? Data()) + tlvRes!.data
if try Data(AES.CMAC(key: ksMac!).authenticate(bytes: K.addPadding())) != tlvMac!.value {
throw RuntimeError(msg: "Invalid MAC value")
throw NFCError.error(msg: "Invalid MAC value")
}
if tlvRes!.value != Data([0x90, 0x00]) {
throw RuntimeError(msg: "\(tlvRes!.value.toHex)")
throw NFCError.error(msg: "\(tlvRes!.value.toHex)")
}
guard tlvEnc != nil else {
return Data()
Expand All @@ -331,10 +391,10 @@ class NFCSignature : NSObject, NFCTagReaderSessionDelegate {

func selectEF(tag: NFCISO7816Tag, file: Bytes) async throws -> Int {
let data = try await sendWrapped(tag: tag, cls: 0x00, ins: 0xA4, p1: 0x02, p2: 0x04, data: file, le: 256)
guard let fci = TKBERTLVRecord(from: data) else {
guard let fci = TLV(from: data) else {
return 0
}
for tlv in TKBERTLVRecord.sequenceOfRecords(from: fci.value)! where tlv.tag == 0x80 {
for tlv in TLV.sequenceOfRecords(from: fci.value) ?? [] where tlv.tag == 0x80 {
return Int(tlv.value[0]) << 8 | Int(tlv.value[1])
}
return 0
Expand Down Expand Up @@ -399,30 +459,14 @@ extension DataProtocol where Self.Index == Int {
if self[i] == 0x80 {
return self[0..<i]
} else if self[i] != 0x00 {
throw RuntimeError(msg: "Failed to remove padding")
throw NFCError.error(msg: "Failed to remove padding")
}
}
throw RuntimeError(msg: "Failed to remove padding")
throw NFCError.error(msg: "Failed to remove padding")
}
}

extension MutableDataProtocol where Self.Index == Int {
init?(hex: String) {
guard hex.count.isMultiple(of: 2) else { return nil }
let chars = hex.map { $0 }
let bytes = stride(from: 0, to: chars.count, by: 2)
.map { String(chars[$0]) + String(chars[$0 + 1]) }
.compactMap { UInt8($0, radix: 16) }
guard hex.count / bytes.count == 2 else { return nil }
self.init(bytes)
}

func addPadding() -> Self {
var padding = Self(repeating: 0x00, count: AES.BlockSize - count % AES.BlockSize)
padding[0] = 0x80
return self + padding
}

public static func ^ (x: Self, y: Self) -> Self {
var result = x
for i in 0..<result.count {
Expand Down Expand Up @@ -453,6 +497,24 @@ extension MutableDataProtocol where Self.Index == Int {
shifted[last] = self[last] << 1
return shifted
}
}

extension MutableDataProtocol {
init?(hex: String) {
guard hex.count.isMultiple(of: 2) else { return nil }
let chars = hex.map { $0 }
let bytes = stride(from: 0, to: chars.count, by: 2)
.map { String(chars[$0]) + String(chars[$0 + 1]) }
.compactMap { UInt8($0, radix: 16) }
guard hex.count / bytes.count == 2 else { return nil }
self.init(bytes)
}

func addPadding() -> Self {
var padding = Self(repeating: 0x00, count: AES.BlockSize - count % AES.BlockSize)
padding[padding.startIndex] = 0x80
return self + padding
}

mutating func resize(to size: Index) -> Self {
self.removeSubrange(size..<endIndex)
Expand All @@ -472,7 +534,7 @@ extension ECPublicKey {
}

extension NFCISO7816Tag {
func sendCommand(cls: UInt8, ins: UInt8, p1: UInt8, p2: UInt8, data: Data, le: Int = -1) async throws -> Data {
func sendCommand(cls: UInt8, ins: UInt8, p1: UInt8, p2: UInt8, data: Data = Data(), le: Int = -1) async throws -> Data {
let apdu = NFCISO7816APDU(instructionClass: cls, instructionCode: ins, p1Parameter: p1, p2Parameter: p2, data: data, expectedResponseLength: le)
switch try await sendCommand(apdu: apdu) {
case (let data, 0x90, 0x00):
Expand All @@ -482,12 +544,19 @@ extension NFCISO7816Tag {
case (_, 0x6C, let len):
return try await sendCommand(cls: cls, ins: ins, p1: p1, p2: p2, data: data, le: Int(len))
case (_, 0x63, let count) where count & 0xC0 > 0:
throw PinError(attemptsLeft: count & 0x0F)
throw NFCError.pinError(attemptsLeft: count & 0x0F)
case (_, 0x69, 0x83):
throw PinError(attemptsLeft: 0)
throw NFCError.pinError(attemptsLeft: 0)
case (_, let sw1, let sw2):
throw RuntimeError(msg: String(format: "%02X%02X", sw1, sw2))
throw NFCError.error(msg: String(format: "%02X%02X", sw1, sw2))
}
}

func sendCommand(cls: UInt8, ins: UInt8, p1: UInt8, p2: UInt8, records: [TKTLVRecord], le: Int = -1) async throws -> Data {
let data = records.reduce(Data()) { partialResult, record in
partialResult + record.data
}
return try await sendCommand(cls: cls, ins: ins, p1: p1, p2: p2, data: data, le: le)
}

func sendPaceCommand(records: [TKTLVRecord], tagExpected: TKTLVTag) async throws -> Data {
Expand All @@ -497,7 +566,7 @@ extension NFCISO7816Tag {
let result = TKBERTLVRecord(from: response.value), result.tag == tagExpected {
return result.value
}
throw RuntimeError(msg: "Invalid response")
throw NFCError.error(msg: "Invalid response")
}
}

Expand Down Expand Up @@ -557,7 +626,7 @@ class AES {
}
}
if status != kCCSuccess {
throw RuntimeError(msg: "AES.CBC.Error")
throw NFCError.error(msg: "AES.CBC.Error")
}
return outputBuffer.resize(to: bytesWritten)
}
Expand Down

0 comments on commit f4133c6

Please sign in to comment.