Skip to content

Commit

Permalink
Expose function for manually sending APDU's to the YubiKey with no ha…
Browse files Browse the repository at this point in the history
…ndling of multiple read operations.
  • Loading branch information
jensutbult committed Mar 27, 2024
1 parent 573e913 commit e2b819d
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 61 deletions.
30 changes: 30 additions & 0 deletions FullStackTests/Tests/ConnectionFullStackTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import XCTest
import YubiKit
import CryptoTokenKit

@testable import FullStackTests

Expand Down Expand Up @@ -114,4 +115,33 @@ class ConnectionFullStackTests: XCTestCase {
XCTAssert([result1, result2, result3, result4].compactMap { $0 }.count == 1)
}
}

func testSendManually() {
runAsyncTest {
let connection = try await Connection.connection()
// Select Management application
let apdu = APDU(cla: 0x00, ins: 0xa4, p1: 0x04, p2: 0x00, command: Data([0xA0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17]), type: .short)
let result = try await connection.sendManually(apdu: apdu)
XCTAssertEqual(result.responseStatus.status, .ok)
/// Get version number
let deviceInfoApdu = APDU(cla: 0, ins: 0x1d, p1: 0, p2: 0)
let deviceInfoResult = try await connection.sendManually(apdu: deviceInfoApdu)
XCTAssertEqual(deviceInfoResult.responseStatus.status, .ok)
let records = TKBERTLVRecord.sequenceOfRecords(from: deviceInfoResult.data.subdata(in: 1..<deviceInfoResult.data.count))
guard let versionData = records?.filter({ $0.tag == 0x05 }).first?.value else { XCTFail("No YubiKey version record in result."); return }
guard versionData.count == 3 else { XCTFail("Wrong sized return data. Got \(versionData.hexEncodedString)"); return }
let bytes = [UInt8](versionData)
let major = bytes[0]
let minor = bytes[1]
let micro = bytes[2]
print("✅ Got version: \(major).\(minor).\(micro)")
XCTAssertEqual(major, 5)
// Try to select non existing application
let notFoundApdu = APDU(cla: 0x00, ins: 0xa4, p1: 0x04, p2: 0x00, command: Data([0x01, 0x02, 0x03]), type: .short)
let notFoundResult = try await connection.sendManually(apdu: notFoundApdu)
if !(notFoundResult.responseStatus.status == .fileNotFound || notFoundResult.responseStatus.status == .incorrectParameters || notFoundResult.responseStatus.status == .invalidInstruction) {
XCTFail("Unexpected result: \(notFoundResult.responseStatus)")
}
}
}
}
2 changes: 1 addition & 1 deletion FullStackTests/Tests/ManagementFullStackTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class ManagementFullStackTests: XCTestCase {
}

func testTimeouts() throws {
runManagementTest { _, session, _ in
runManagementTest { connection, session, _ in
let deviceInfo = try await session.getDeviceInfo()
let config = deviceInfo.config.deviceConfig(autoEjectTimeout: 320.0, challengeResponseTimeout: 135.0)
try await session.updateDeviceConfig(config, reboot: false)
Expand Down
108 changes: 64 additions & 44 deletions YubiKit/YubiKit/APDU.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,26 @@ import Foundation
/// Data model for encapsulating an APDU command, as defined by the ISO/IEC 7816-4 standard.
public struct APDU: CustomStringConvertible {

public var description: String {
return "APDU(cla: \(cla.hexValue), ins: \(ins.hexValue), p1: \(p1.hexValue), p2: \(p2.hexValue), command: \(data.hexEncodedString), type: \(String(describing: type))"
}

public enum ApduType {
case short
case extended
}

private let cla: UInt8
private let ins: UInt8
private let p1: UInt8
private let p2: UInt8
private let command: Data?
private let type: ApduType
private struct ExplicitAPDU {
let cla: UInt8
let ins: UInt8
let p1: UInt8
let p2: UInt8
let command: Data?
let type: ApduType
}

private enum APDUStorage {
case explicit(ExplicitAPDU)
case rawData(Data)
}

private let storage: APDUStorage

/// Creates an APDU struct.
/// - Parameters:
Expand All @@ -43,44 +48,59 @@ public struct APDU: CustomStringConvertible {
/// - command: The command data.
/// - type: The type of the APDU, short or extended.
public init(cla: UInt8, ins: UInt8, p1: UInt8, p2: UInt8, command: Data? = nil, type: ApduType = .short) {
self.cla = cla
self.ins = ins
self.p1 = p1
self.p2 = p2
self.command = command
self.type = type
self.storage = .explicit(ExplicitAPDU(cla: cla, ins: ins, p1: p1, p2: p2, command: command, type: type))
}

/// Creates an APDU struct.
/// - Parameters:
/// - data: The raw data to send to they YubiKey.
public init(data: Data) {
self.storage = .rawData(data)
}

public var data: Data {
var data = Data()
data.append(cla)
data.append(ins)
data.append(p1)
data.append(p2)

switch type {
case .short:
if let command, command.count > 0 {
guard command.count < UInt8.max else { fatalError() }
let length = UInt8(command.count)
data.append(length)
data.append(command)
}
case .extended:
if let command, command.count > 0 {
let lengthHigh: UInt8 = UInt8(command.count / 256)
let lengthLow: UInt8 = UInt8(command.count % 256)
data.append(0x00)
data.append(lengthHigh)
data.append(lengthLow)
data.append(command)
} else {
data.append(0x00)
data.append(0x00)
data.append(0x00)
switch storage {
case .explicit(let apdu):
var data = Data()
data.append(apdu.cla)
data.append(apdu.ins)
data.append(apdu.p1)
data.append(apdu.p2)
switch apdu.type {
case .short:
if let command = apdu.command, command.count > 0 {
guard command.count < UInt8.max else { fatalError() }
let length = UInt8(command.count)
data.append(length)
data.append(command)
}
case .extended:
if let command = apdu.command, command.count > 0 {
let lengthHigh: UInt8 = UInt8(command.count / 256)
let lengthLow: UInt8 = UInt8(command.count % 256)
data.append(0x00)
data.append(lengthHigh)
data.append(lengthLow)
data.append(command)
} else {
data.append(0x00)
data.append(0x00)
data.append(0x00)
}
}
}

return data
return data
case .rawData(let data):
return data
}
}

public var description: String {
switch storage {
case .explicit(let apdu):
return "APDU(cla: \(apdu.cla.hexValue), ins: \(apdu.ins.hexValue), p1: \(apdu.p1.hexValue), p2: \(apdu.p2.hexValue), command: \(apdu.command?.hexEncodedString ?? "nil"), type: \(String(describing: apdu.type))"
case .rawData(let data):
return "APDU(data: \(data.hexEncodedString))"
}
}
}
16 changes: 8 additions & 8 deletions YubiKit/YubiKit/Connection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,19 @@ public protocol Connection: AnyObject {
/// wrapping the status code will be thrown.
@discardableResult
func send(apdu: APDU) async throws -> Data

/// Send an APDU to the Connection and handle the result manually.
///
/// This will send the APDU to the YubiKey using the Connection. The result will be wrapped in
/// a Response containing the Data and the ResponseStatus. If the returned data is to big for a
/// single read operation this has to be handled manually.
@discardableResult
func sendManually(apdu: APDU) async throws -> Response
}

internal protocol InternalConnection {

func session() async -> Session?
func setSession(_ session: Session?) async

// The internal version of the send() function returns a Response instead of Data. The reason for this is
// to handle reads of large chunks of data that will be split into multiple reads. If the result is
// to large for a single read that is signaled by sw1 being 0x61. The Response struct will return both
// the data and sw1 and sw2. sendRecursive() in Connection+Extensions will look at the sw1 code and if
// it indicates there's more data to read, it will call itself recursivly to retrieve the next chunk of data.
func send(apdu: APDU) async throws -> Response
}

extension InternalConnection {
Expand Down
2 changes: 1 addition & 1 deletion YubiKit/YubiKit/LightningConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public final actor LightningConnection: Connection, InternalConnection {
}
}

internal func send(apdu: APDU) async throws -> Response {
public func sendManually(apdu: APDU) async throws -> Response {
guard let accessoryConnection, let outputStream = accessoryConnection.session.outputStream, let inputStream = accessoryConnection.session.inputStream else { throw ConnectionError.noConnection }
var data = Data([0x00]) // YLP iAP2 Signal
data.append(apdu.data)
Expand Down
2 changes: 1 addition & 1 deletion YubiKit/YubiKit/NFCConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public final actor NFCConnection: Connection, InternalConnection {
}
}

internal func send(apdu: APDU) async throws -> Response {
public func sendManually(apdu: APDU) async throws -> Response {
guard let tag else { throw ConnectionError.noConnection }
guard let apdu = apdu.nfcIso7816Apdu else { throw NFCConnectionError.malformedAPDU }
let result: (Data, UInt8, UInt8) = try await tag.sendCommand(apdu: apdu)
Expand Down
7 changes: 4 additions & 3 deletions YubiKit/YubiKit/Response.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import Foundation

internal struct Response: CustomStringConvertible {
public struct Response: CustomStringConvertible {

internal init(rawData: Data) {
if rawData.count > 2 {
Expand All @@ -31,16 +31,17 @@ internal struct Response: CustomStringConvertible {
}

/// The data returned in the response.
/// >Note: The data does not contain the response code. It is stored in the `ResponseStatus`.
public let data: Data

/// Status code of the response
internal let responseStatus: ResponseStatus
public let responseStatus: ResponseStatus
public var description: String {
return "<Response: \(responseStatus.status) \(responseStatus.rawStatus.data.hexEncodedString), length: \(data.count)>"
}
}

public struct ResponseStatus {
public struct ResponseStatus: Equatable {
public enum StatusCode: UInt16 {
case ok = 0x9000
case noInputData = 0x6285
Expand Down
2 changes: 1 addition & 1 deletion YubiKit/YubiKit/SmartCardConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public final actor SmartCardConnection: Connection, InternalConnection {
}
}

internal func send(apdu: APDU) async throws -> Response {
public func sendManually(apdu: APDU) async throws -> Response {
guard let smartCard else { throw ConnectionError.noConnection }
let data = try await smartCard.transmit(apdu.data)
return Response(rawData: data)
Expand Down
4 changes: 2 additions & 2 deletions YubiKit/YubiKit/Utilities/Connection+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ extension Connection {
}
if readMoreData {
let apdu = APDU(cla: 0, ins: ins, p1: 0, p2: 0, command: nil, type: .short)
response = try await internalConnection.send(apdu: apdu)
response = try await self.sendManually(apdu: apdu)
} else {
response = try await internalConnection.send(apdu: apdu)
response = try await self.sendManually(apdu: apdu)
}

guard response.responseStatus.status == .ok || response.responseStatus.sw1 == 0x61 else {
Expand Down

0 comments on commit e2b819d

Please sign in to comment.