From 41c6fb36ce24e6e463523bb006558d0a71df3eda Mon Sep 17 00:00:00 2001 From: Jens Utbult Date: Tue, 26 Mar 2024 17:42:05 +0100 Subject: [PATCH 1/5] Added support for sending raw commands to the YubiKey. --- Changelog.md | 4 ++++ .../YKFAccessoryConnection.m | 14 ++++++++++++++ .../NFCConnection/YKFNFCConnection.m | 14 ++++++++++++++ .../YKFSmartCardConnection.m | 14 ++++++++++++++ .../Connections/YKFConnectionProtocol.h | 19 ++++++++++++++++++- 5 files changed, 64 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 324bc4f2..983f7490 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,9 @@ # YubiKit Changelog +## 4.5.0 + +- Add support for sending and returning raw commands to the `YKFConnectionProtocol`. + ## 4.4.1 - Removed deprecated PCSCLayer. diff --git a/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnection.m b/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnection.m index 576caf11..be551bba 100644 --- a/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnection.m +++ b/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnection.m @@ -182,6 +182,20 @@ - (void)managementSession:(YKFManagementSessionCompletion _Nonnull)callback { }]; } +- (void)executeRawCommand:(YKFAPDU *)apdu completion:(YKFRawComandCompletion)completion { + [self.connectionController execute:apdu completion:^(NSData * _Nullable data, NSError * _Nullable error, NSTimeInterval executionTime) { + completion(data, error); + }]; +} + +- (void)executeRawCommand:(YKFAPDU *)apdu timeout:(NSTimeInterval)timeout completion:(YKFRawComandCompletion)completion { + [self.connectionController execute:apdu + timeout:timeout + completion:^(NSData * _Nullable response, NSError * _Nullable error, NSTimeInterval executionTime) { + completion(response, error); + }]; +} + - (void)dealloc { self.observeAccessoryConnection = NO; self.observeApplicationState = NO; diff --git a/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection.m b/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection.m index 73a4a7d4..5ab208e5 100644 --- a/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection.m +++ b/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection.m @@ -138,6 +138,20 @@ - (void)managementSession:(YKFManagementSessionCompletion _Nonnull)callback { }]; } +- (void)executeRawCommand:(YKFAPDU *)apdu completion:(YKFRawComandCompletion)completion { + [self.connectionController execute:apdu completion:^(NSData * _Nullable data, NSError * _Nullable error, NSTimeInterval executionTime) { + completion(data, error); + }]; +} + +- (void)executeRawCommand:(YKFAPDU *)apdu timeout:(NSTimeInterval)timeout completion:(YKFRawComandCompletion)completion { + [self.connectionController execute:apdu + timeout:timeout + completion:^(NSData * _Nullable response, NSError * _Nullable error, NSTimeInterval executionTime) { + completion(response, error); + }]; +} + - (void)dealloc { if (@available(iOS 13.0, *)) { [self unobserveIso7816TagAvailability]; diff --git a/YubiKit/YubiKit/Connections/SmartCardConnection/YKFSmartCardConnection.m b/YubiKit/YubiKit/Connections/SmartCardConnection/YKFSmartCardConnection.m index 2853af10..67163883 100644 --- a/YubiKit/YubiKit/Connections/SmartCardConnection/YKFSmartCardConnection.m +++ b/YubiKit/YubiKit/Connections/SmartCardConnection/YKFSmartCardConnection.m @@ -169,4 +169,18 @@ - (void)u2fSession:(YKFU2FSessionCompletionBlock _Nonnull)completion { userInfo:@{NSLocalizedDescriptionKey: @"U2F session not supported by YKFSmartCardConnection."}]); } +- (void)executeRawCommand:(YKFAPDU *)apdu completion:(YKFRawComandCompletion)completion { + [self.connectionController execute:apdu completion:^(NSData * _Nullable data, NSError * _Nullable error, NSTimeInterval executionTime) { + completion(data, error); + }]; +} + +- (void)executeRawCommand:(YKFAPDU *)apdu timeout:(NSTimeInterval)timeout completion:(YKFRawComandCompletion)completion { + [self.connectionController execute:apdu + timeout:timeout + completion:^(NSData * _Nullable response, NSError * _Nullable error, NSTimeInterval executionTime) { + completion(response, error); + }]; +} + @end diff --git a/YubiKit/YubiKit/Connections/YKFConnectionProtocol.h b/YubiKit/YubiKit/Connections/YKFConnectionProtocol.h index a6804645..da4d9b43 100644 --- a/YubiKit/YubiKit/Connections/YKFConnectionProtocol.h +++ b/YubiKit/YubiKit/Connections/YKFConnectionProtocol.h @@ -15,7 +15,7 @@ #ifndef YKFConnectionProtocol_h #define YKFConnectionProtocol_h -@class YKFOATHSession, YKFU2FSession, YKFFIDO2Session, YKFPIVSession, YKFChallengeResponseSession, YKFManagementSession, YKFSmartCardInterface; +@class YKFOATHSession, YKFU2FSession, YKFFIDO2Session, YKFPIVSession, YKFChallengeResponseSession, YKFManagementSession, YKFSmartCardInterface, YKFAPDU; @protocol YKFConnectionProtocol @@ -66,6 +66,23 @@ typedef void (^YKFManagementSessionCompletion)(YKFManagementSession *_Nullable, /// when none of the supplied sessions can be used. @property (nonatomic, readonly) YKFSmartCardInterface *_Nullable smartCardInterface; +typedef void (^YKFRawComandCompletion)(NSData *_Nullable, NSError *_Nullable); + +/// @abstract Send a APDU and get the unparsed result as an NSData from the YubiKey. +/// @param apdu The APDU to send to the YubiKey. +/// @param completion The unparsed result from the YubiKey or an error. +/// @discussion Use this for communicating with the YubiKey by sending APDUs to the it. Only use this +/// when the `SmartCardInterface` or any of the supplied sessions can not be used. +- (void)executeRawCommand:(YKFAPDU *_Nonnull)apdu completion:(YKFRawComandCompletion _Nonnull)completion; + +/// @abstract Send a APDU and get the unparsed result as an NSData from the YubiKey. +/// @param apdu The APDU to send to the YubiKey. +/// @param timeout The timeout to wait before cancelling the command sent to the YubiKey. +/// @param completion The unparsed result from the YubiKey or an error. +/// @discussion Use this for communicating with the YubiKey by sending APDUs to the it. Only use this +/// when the `SmartCardInterface` or any of the supplied sessions can not be used. +- (void)executeRawCommand:(YKFAPDU *_Nonnull)apdu timeout:(NSTimeInterval)timeout completion:(YKFRawComandCompletion _Nonnull)completion; + @end #endif From 3df82a5b7eaf6a9c9bfeb3f514fcba0e5f347c9c Mon Sep 17 00:00:00 2001 From: Jens Utbult Date: Wed, 27 Mar 2024 10:39:14 +0100 Subject: [PATCH 2/5] Test for sending raw commands. --- YubiKitTests/Tests/ConnectionTests.swift | 53 ++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/YubiKitTests/Tests/ConnectionTests.swift b/YubiKitTests/Tests/ConnectionTests.swift index 737bfc24..ad11a0d2 100644 --- a/YubiKitTests/Tests/ConnectionTests.swift +++ b/YubiKitTests/Tests/ConnectionTests.swift @@ -17,7 +17,9 @@ import XCTest class ConnectionTests: XCTestCase { func testConnectionDelegate() throws { - YubiKitManager.shared.startAccessoryConnection() + if YubiKitDeviceCapabilities.supportsMFIAccessoryKey { + YubiKitManager.shared.startAccessoryConnection() + } YubiKitManager.shared.startSmartCardConnection() let connectionExpectation = expectation(description: "Get a YubiKey Connection") let firstConnection = YubiKeyConnectionTester() @@ -45,7 +47,9 @@ class ConnectionTests: XCTestCase { func testNFCTimeOutError() throws { let connectionExpectation = expectation(description: "Get a YubiKey Connection") let connectionTester = YubiKeyConnectionTester() - YubiKitManager.shared.startAccessoryConnection() // We need to start the accessory connection so we can skip this test if a 5Ci key is inserted + if YubiKitDeviceCapabilities.supportsMFIAccessoryKey { + YubiKitManager.shared.startAccessoryConnection() // We need to start the accessory connection so we can skip this test if a 5Ci key is inserted + } YubiKitManager.shared.startSmartCardConnection() Thread.sleep(forTimeInterval: 0.5) @@ -76,7 +80,9 @@ class ConnectionTests: XCTestCase { func testNFCUserCancelError() throws { let connectionExpectation = expectation(description: "Got a YubiKey failed to connect to NFC error") let connectionTester = YubiKeyConnectionTester() - YubiKitManager.shared.startAccessoryConnection() // We need to start the accessory connection so we can skip this test if a 5Ci key is inserted + if YubiKitDeviceCapabilities.supportsMFIAccessoryKey { + YubiKitManager.shared.startAccessoryConnection() // We need to start the accessory connection so we can skip this test if a 5Ci key is inserted + } YubiKitManager.shared.startSmartCardConnection() Thread.sleep(forTimeInterval: 0.5) @@ -101,6 +107,35 @@ class ConnectionTests: XCTestCase { } } } + + func testRawCommands() throws { + if YubiKitDeviceCapabilities.supportsMFIAccessoryKey { + YubiKitManager.shared.startAccessoryConnection() + } + YubiKitManager.shared.startSmartCardConnection() + let connectionExpectation = expectation(description: "Get a YubiKey Connection") + let firstConnection = YubiKeyConnectionTester() + Thread.sleep(forTimeInterval: 0.5) + firstConnection.connection { connection, error in + // Select Management application + let data = Data([0xA0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17]) + let apdu = YKFAPDU(cla: 0x00, ins: 0xa4, p1: 0x04, p2: 0x00, data: data, type: .short)! + connection.executeRawCommand(apdu) { data, error in + guard let data else { XCTFail("Failed with error: \(error!)"); return } + XCTAssertEqual(data.statusCode, 0x9000) + connectionExpectation.fulfill() + } + } + + waitForExpectations(timeout: 20.0) { error in + // If we get an error then the expectation has timed out and we need to stop all connections + if error != nil { + YubiKitManager.shared.stopNFCConnection() + Thread.sleep(forTimeInterval: 5.0) // In case it was a NFC connection wait for the modal to dismiss + } + } + } + } class YubiKeyConnectionTester: NSObject { @@ -188,3 +223,15 @@ extension YubiKeyConnectionTester: YKFManagerDelegate { smartCardConnection = nil } } + +extension Data { + /// Returns the SW from a key response. + var statusCode: UInt16 { + get { + guard self.count >= 2 else { + return 0x00 + } + return UInt16(self[self.count - 2]) << 8 + UInt16(self[self.count - 1]) + } + } +} From 888e225671a50b18703c640532557f31c7243675 Mon Sep 17 00:00:00 2001 From: Jens Utbult Date: Wed, 27 Mar 2024 10:43:31 +0100 Subject: [PATCH 3/5] Bumped version to 4.5.0. --- README.md | 2 +- YubiKit.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3035116a..eca40ab2 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Add YubiKit to your [Podfile](https://guides.cocoapods.org/using/the-podfile.htm ```ruby use_frameworks! -pod 'YubiKit', '~> 4.4.1' +pod 'YubiKit', '~> 4.5.0' ``` If you want to have latest changes, replace the last line with: diff --git a/YubiKit.podspec b/YubiKit.podspec index bb7eba7e..071bda9d 100644 --- a/YubiKit.podspec +++ b/YubiKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'YubiKit' - s.version = '4.4.1' + s.version = '4.5.0' s.license = 'Apache 2.0' s.summary = 'YubiKit is an iOS library provided by Yubico to interact with YubiKeys on iOS devices.' s.homepage = 'https://github.com/Yubico/yubikit-ios' From 9dc3c9706da53e5858d54b0ee3f510965411a5e1 Mon Sep 17 00:00:00 2001 From: Jens Utbult Date: Wed, 27 Mar 2024 10:51:54 +0100 Subject: [PATCH 4/5] Make YKFConnectionProtocol public. --- YubiKit/YubiKit/YubiKit.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/YubiKit/YubiKit/YubiKit.h b/YubiKit/YubiKit/YubiKit.h index 02cbf1a4..ca639319 100644 --- a/YubiKit/YubiKit/YubiKit.h +++ b/YubiKit/YubiKit/YubiKit.h @@ -29,6 +29,8 @@ #import "YKFNFCError.h" #import "YKFNFCTagDescription.h" +#import "YKFConnectionProtocol.h" + #import "YKFAccessoryConnection.h" #import "YKFAccessoryDescription.h" From d557f8cdbd9b2b665e8995a96b5a1086fba5db43 Mon Sep 17 00:00:00 2001 From: Jens Utbult Date: Thu, 28 Mar 2024 10:17:03 +0100 Subject: [PATCH 5/5] Send NSData instead of YKFAPDU as raw command. --- .../AccessoryConnection/YKFAccessoryConnection.m | 6 ++++-- .../YubiKit/Connections/NFCConnection/YKFNFCConnection.m | 6 ++++-- .../SmartCardConnection/YKFSmartCardConnection.m | 6 ++++-- YubiKit/YubiKit/Connections/YKFConnectionProtocol.h | 8 ++++---- YubiKitTests/Tests/ConnectionTests.swift | 6 +++--- 5 files changed, 19 insertions(+), 13 deletions(-) diff --git a/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnection.m b/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnection.m index be551bba..e14af7ee 100644 --- a/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnection.m +++ b/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnection.m @@ -182,13 +182,15 @@ - (void)managementSession:(YKFManagementSessionCompletion _Nonnull)callback { }]; } -- (void)executeRawCommand:(YKFAPDU *)apdu completion:(YKFRawComandCompletion)completion { +- (void)executeRawCommand:(NSData *)data completion:(YKFRawComandCompletion)completion { + YKFAPDU *apdu = [[YKFAPDU alloc] initWithData:data]; [self.connectionController execute:apdu completion:^(NSData * _Nullable data, NSError * _Nullable error, NSTimeInterval executionTime) { completion(data, error); }]; } -- (void)executeRawCommand:(YKFAPDU *)apdu timeout:(NSTimeInterval)timeout completion:(YKFRawComandCompletion)completion { +- (void)executeRawCommand:(NSData *)data timeout:(NSTimeInterval)timeout completion:(YKFRawComandCompletion)completion { + YKFAPDU *apdu = [[YKFAPDU alloc] initWithData:data]; [self.connectionController execute:apdu timeout:timeout completion:^(NSData * _Nullable response, NSError * _Nullable error, NSTimeInterval executionTime) { diff --git a/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection.m b/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection.m index 5ab208e5..54cb5ba9 100644 --- a/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection.m +++ b/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection.m @@ -138,13 +138,15 @@ - (void)managementSession:(YKFManagementSessionCompletion _Nonnull)callback { }]; } -- (void)executeRawCommand:(YKFAPDU *)apdu completion:(YKFRawComandCompletion)completion { +- (void)executeRawCommand:(NSData *)data completion:(YKFRawComandCompletion)completion { + YKFAPDU *apdu = [[YKFAPDU alloc] initWithData:data]; [self.connectionController execute:apdu completion:^(NSData * _Nullable data, NSError * _Nullable error, NSTimeInterval executionTime) { completion(data, error); }]; } -- (void)executeRawCommand:(YKFAPDU *)apdu timeout:(NSTimeInterval)timeout completion:(YKFRawComandCompletion)completion { +- (void)executeRawCommand:(NSData *)data timeout:(NSTimeInterval)timeout completion:(YKFRawComandCompletion)completion { + YKFAPDU *apdu = [[YKFAPDU alloc] initWithData:data]; [self.connectionController execute:apdu timeout:timeout completion:^(NSData * _Nullable response, NSError * _Nullable error, NSTimeInterval executionTime) { diff --git a/YubiKit/YubiKit/Connections/SmartCardConnection/YKFSmartCardConnection.m b/YubiKit/YubiKit/Connections/SmartCardConnection/YKFSmartCardConnection.m index 67163883..d9b5144c 100644 --- a/YubiKit/YubiKit/Connections/SmartCardConnection/YKFSmartCardConnection.m +++ b/YubiKit/YubiKit/Connections/SmartCardConnection/YKFSmartCardConnection.m @@ -169,13 +169,15 @@ - (void)u2fSession:(YKFU2FSessionCompletionBlock _Nonnull)completion { userInfo:@{NSLocalizedDescriptionKey: @"U2F session not supported by YKFSmartCardConnection."}]); } -- (void)executeRawCommand:(YKFAPDU *)apdu completion:(YKFRawComandCompletion)completion { +- (void)executeRawCommand:(NSData *)data completion:(YKFRawComandCompletion)completion { + YKFAPDU *apdu = [[YKFAPDU alloc] initWithData:data]; [self.connectionController execute:apdu completion:^(NSData * _Nullable data, NSError * _Nullable error, NSTimeInterval executionTime) { completion(data, error); }]; } -- (void)executeRawCommand:(YKFAPDU *)apdu timeout:(NSTimeInterval)timeout completion:(YKFRawComandCompletion)completion { +- (void)executeRawCommand:(NSData *)data timeout:(NSTimeInterval)timeout completion:(YKFRawComandCompletion)completion { + YKFAPDU *apdu = [[YKFAPDU alloc] initWithData:data]; [self.connectionController execute:apdu timeout:timeout completion:^(NSData * _Nullable response, NSError * _Nullable error, NSTimeInterval executionTime) { diff --git a/YubiKit/YubiKit/Connections/YKFConnectionProtocol.h b/YubiKit/YubiKit/Connections/YKFConnectionProtocol.h index da4d9b43..43ed5eae 100644 --- a/YubiKit/YubiKit/Connections/YKFConnectionProtocol.h +++ b/YubiKit/YubiKit/Connections/YKFConnectionProtocol.h @@ -73,15 +73,15 @@ typedef void (^YKFRawComandCompletion)(NSData *_Nullable, NSError *_Nullable); /// @param completion The unparsed result from the YubiKey or an error. /// @discussion Use this for communicating with the YubiKey by sending APDUs to the it. Only use this /// when the `SmartCardInterface` or any of the supplied sessions can not be used. -- (void)executeRawCommand:(YKFAPDU *_Nonnull)apdu completion:(YKFRawComandCompletion _Nonnull)completion; +- (void)executeRawCommand:(NSData *_Nonnull)apdu completion:(YKFRawComandCompletion _Nonnull)completion; -/// @abstract Send a APDU and get the unparsed result as an NSData from the YubiKey. -/// @param apdu The APDU to send to the YubiKey. +/// @abstract Send command as NSData and get the unparsed result as an NSData from the YubiKey. +/// @param data The NSData to send to the YubiKey. /// @param timeout The timeout to wait before cancelling the command sent to the YubiKey. /// @param completion The unparsed result from the YubiKey or an error. /// @discussion Use this for communicating with the YubiKey by sending APDUs to the it. Only use this /// when the `SmartCardInterface` or any of the supplied sessions can not be used. -- (void)executeRawCommand:(YKFAPDU *_Nonnull)apdu timeout:(NSTimeInterval)timeout completion:(YKFRawComandCompletion _Nonnull)completion; +- (void)executeRawCommand:(NSData *_Nonnull)data timeout:(NSTimeInterval)timeout completion:(YKFRawComandCompletion _Nonnull)completion; @end diff --git a/YubiKitTests/Tests/ConnectionTests.swift b/YubiKitTests/Tests/ConnectionTests.swift index ad11a0d2..1d001233 100644 --- a/YubiKitTests/Tests/ConnectionTests.swift +++ b/YubiKitTests/Tests/ConnectionTests.swift @@ -118,10 +118,10 @@ class ConnectionTests: XCTestCase { Thread.sleep(forTimeInterval: 0.5) firstConnection.connection { connection, error in // Select Management application - let data = Data([0xA0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17]) - let apdu = YKFAPDU(cla: 0x00, ins: 0xa4, p1: 0x04, p2: 0x00, data: data, type: .short)! - connection.executeRawCommand(apdu) { data, error in + let data = Data([0x00, 0xa4, 0x04, 0x00, 0x08, 0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17]) + connection.executeRawCommand(data) { data, error in guard let data else { XCTFail("Failed with error: \(error!)"); return } + print(data.hexDescription) XCTAssertEqual(data.statusCode, 0x9000) connectionExpectation.fulfill() }