diff --git a/YubiKit/YubiKit.xcodeproj/project.pbxproj b/YubiKit/YubiKit.xcodeproj/project.pbxproj index 312e2ddb..b77dbbe2 100644 --- a/YubiKit/YubiKit.xcodeproj/project.pbxproj +++ b/YubiKit/YubiKit.xcodeproj/project.pbxproj @@ -205,6 +205,8 @@ A54DCC0323F2147500E95259 /* YKNSStringAdditionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A54DCC0223F2147500E95259 /* YKNSStringAdditionTests.m */; }; B41B6F9A27A96B760062C377 /* YKFTLVRecord.m in Sources */ = {isa = PBXBuildFile; fileRef = B41B6F9927A96B760062C377 /* YKFTLVRecord.m */; }; B41B6F9C27A97DB40062C377 /* YKFTLVRecordTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B41B6F9B27A97DB40062C377 /* YKFTLVRecordTests.m */; }; + B428498C2C22DA730000F8CF /* YKFPIVBioMetadata.m in Sources */ = {isa = PBXBuildFile; fileRef = B428498B2C22DA730000F8CF /* YKFPIVBioMetadata.m */; }; + B428498F2C2305EA0000F8CF /* YKFInvalidPinError.m in Sources */ = {isa = PBXBuildFile; fileRef = B428498E2C2305EA0000F8CF /* YKFInvalidPinError.m */; }; B4451EBE2757B493002690BB /* YKFConnectionProtocol.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5176559925405FE400819857 /* YKFConnectionProtocol.h */; }; B4451EBF2757B4A9002690BB /* YKFPIVError.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 51ACC34925E7EC910069214B /* YKFPIVError.h */; }; B4451EC02757B4BF002690BB /* YKFPIVSession.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 51ACC2FB25D5860C0069214B /* YKFPIVSession.h */; }; @@ -633,6 +635,11 @@ B41B6F9827A96B5B0062C377 /* YKFTLVRecord.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFTLVRecord.h; sourceTree = ""; }; B41B6F9927A96B760062C377 /* YKFTLVRecord.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFTLVRecord.m; sourceTree = ""; }; B41B6F9B27A97DB40062C377 /* YKFTLVRecordTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFTLVRecordTests.m; sourceTree = ""; }; + B428498A2C22DA730000F8CF /* YKFPIVBioMetadata.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFPIVBioMetadata.h; sourceTree = ""; }; + B428498B2C22DA730000F8CF /* YKFPIVBioMetadata.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFPIVBioMetadata.m; sourceTree = ""; }; + B428498D2C22DC1B0000F8CF /* YKFPIVBioMetadata+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFPIVBioMetadata+Private.h"; sourceTree = ""; }; + B428498E2C2305EA0000F8CF /* YKFInvalidPinError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFInvalidPinError.m; sourceTree = ""; }; + B42849902C23061B0000F8CF /* YKFInvalidPinError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFInvalidPinError.h; sourceTree = ""; }; B4712B6C28DC8412009B270D /* YKFOATHSetAccessKeyAPDU.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YKFOATHSetAccessKeyAPDU.h; sourceTree = ""; }; B4712B6D28DC8413009B270D /* YKFOATHSetAccessKeyAPDU.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YKFOATHSetAccessKeyAPDU.m; sourceTree = ""; }; B4C9BBC92A05547400FFDFD6 /* NSData+GZIP.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSData+GZIP.m"; sourceTree = ""; }; @@ -699,6 +706,9 @@ 5110D6982600D9C800467680 /* YKFPIVPadding.m */, 5110D6AF2603566200467680 /* YKFPIVKeyType.h */, 5110D6B02603568800467680 /* YKFPIVKeyType.m */, + B428498A2C22DA730000F8CF /* YKFPIVBioMetadata.h */, + B428498D2C22DC1B0000F8CF /* YKFPIVBioMetadata+Private.h */, + B428498B2C22DA730000F8CF /* YKFPIVBioMetadata.m */, ); path = PIV; sourceTree = ""; @@ -1302,6 +1312,8 @@ 8152341123BAE9D2004D4788 /* YKFChallengeResponseError.m */, 51ACC34925E7EC910069214B /* YKFPIVError.h */, 51ACC34A25E7ECA80069214B /* YKFPIVError.m */, + B42849902C23061B0000F8CF /* YKFInvalidPinError.h */, + B428498E2C2305EA0000F8CF /* YKFInvalidPinError.m */, ); path = Errors; sourceTree = ""; @@ -1483,6 +1495,7 @@ 8152340B23B573E4004D4788 /* YKFChalRespRequest.m in Sources */, 5121B2212563DE8200300145 /* YKFSmartCardInterface.m in Sources */, 95DD40872099A86A00363FEE /* YKFU2FSignAPDU.m in Sources */, + B428498C2C22DA730000F8CF /* YKFPIVBioMetadata.m in Sources */, 954E2C542211AA5600720D2B /* YKFFIDO2ClientPinAPDU.m in Sources */, 51ACC32925DC01DA0069214B /* YKFPIVSessionFeatures.m in Sources */, 5110D67625F8FD2F00467680 /* YKFPIVManagementKeyMetadata.m in Sources */, @@ -1504,6 +1517,7 @@ 9581395421591DE1008558F3 /* YKFSelectOATHApplicationAPDU.m in Sources */, 95BA204521F7483100EED927 /* YKFFIDO2GetAssertionResponse.m in Sources */, 51ACC33625E50C860069214B /* NSArray+YKFTLVRecord.m in Sources */, + B428498F2C2305EA0000F8CF /* YKFInvalidPinError.m in Sources */, 815233FE23B56A6F004D4788 /* YKFChalRespSendRequest.m in Sources */, 95081DEE2214255B006CD08C /* YKFRequest.m in Sources */, 956991F422C224BC00C5EB02 /* YKFWebAuthnClientData.m in Sources */, diff --git a/YubiKit/YubiKit/Connections/Shared/Errors/YKFAPDUError.h b/YubiKit/YubiKit/Connections/Shared/Errors/YKFAPDUError.h index f3e06ea6..46ff5007 100644 --- a/YubiKit/YubiKit/Connections/Shared/Errors/YKFAPDUError.h +++ b/YubiKit/YubiKit/Connections/Shared/Errors/YKFAPDUError.h @@ -27,6 +27,7 @@ typedef NS_ENUM(NSUInteger, YKFAPDUErrorCode) { YKFAPDUErrorCodeCLANotSupported = 0x6E00, YKFAPDUErrorCodeCommandAborted = 0x6F00, YKFAPDUErrorCodeMissingFile = 0x6A82, + YKFAPDUErrorCodeReferencedDataNotFound = 0x6a88, // Application/Applet short codes diff --git a/YubiKit/YubiKit/Connections/Shared/Errors/YKFInvalidPinError.h b/YubiKit/YubiKit/Connections/Shared/Errors/YKFInvalidPinError.h new file mode 100644 index 00000000..1a1559ad --- /dev/null +++ b/YubiKit/YubiKit/Connections/Shared/Errors/YKFInvalidPinError.h @@ -0,0 +1,28 @@ +// Copyright 2018-2024 Yubico AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef YKFInvalidPinError_h +#define YKFInvalidPinError_h + +extern NSString* const YKFInvalidPinErrorDomain; +extern NSInteger const YKFInvalidPinErrorCode; + +@interface YKFInvalidPinError: NSError + +@property (nonatomic, readonly) int retries; + ++ (instancetype)invalidPinErrorWithRetries:(int)retries; + +@end +#endif /* YKFInvalidPinError_h */ diff --git a/YubiKit/YubiKit/Connections/Shared/Errors/YKFInvalidPinError.m b/YubiKit/YubiKit/Connections/Shared/Errors/YKFInvalidPinError.m new file mode 100644 index 00000000..6a7e5644 --- /dev/null +++ b/YubiKit/YubiKit/Connections/Shared/Errors/YKFInvalidPinError.m @@ -0,0 +1,35 @@ +// Copyright 2018-2024 Yubico AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import +#import "YKFInvalidPinError.h" + +NSString* const YKFInvalidPinErrorDomain = @"com.yubico.invalid-pin"; +NSInteger const YKFInvalidPinErrorCode = 1; + +@interface YKFInvalidPinError() + +@property (nonatomic, readwrite) int retries; + +@end + +@implementation YKFInvalidPinError + ++ (instancetype)invalidPinErrorWithRetries:(int)retries { + YKFInvalidPinError *error = [[YKFInvalidPinError alloc] initWithDomain:YKFInvalidPinErrorDomain code:YKFInvalidPinErrorCode userInfo:nil]; + error.retries = retries; + return error; +} + +@end diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementSession.h b/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementSession.h index 5a1101e9..6eeb6734 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementSession.h +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementSession.h @@ -47,6 +47,14 @@ typedef void (^YKFManagementSessionGetDeviceInfoBlock) /// parameter is nil. typedef void (^YKFManagementSessionWriteCompletionBlock) (NSError* _Nullable error); +/// @abstract +/// Response block for [deviceReset:completion:] which will do a device reset on a YubiKey Bio +/// +/// @param error +/// In case of a failed request this parameter contains the error. If the request was successful this +/// parameter is nil. +typedef void (^YKFManagementSessionDeviceResetCompletionBlock) (NSError* _Nullable error); + NS_ASSUME_NONNULL_BEGIN /// @abstract Defines the interface for YKFManagementSessionProtocol. @@ -127,6 +135,19 @@ NS_ASSUME_NONNULL_BEGIN /// The method is thread safe and can be invoked from any thread (main or a background thread). - (void)writeConfiguration:(YKFManagementInterfaceConfiguration*)configuration reboot:(BOOL)reboot completion:(nonnull YKFManagementSessionWriteCompletionBlock)completion; +/// @abstract +/// Perform a device-wide reset in Bio Multi-protocol Edition devices +/// +/// @param completion +/// The response block which is executed after the request was processed by the key. The completion block +/// will be executed on a background thread. +/// +/// @note: +/// This method requires support for device reset, available in YubiKey 5.6 or later. +/// The method is thread safe and can be invoked from any thread (main or a background thread). +- (void)deviceReset:(YKFManagementSessionDeviceResetCompletionBlock)completion; + + - (instancetype)init NS_UNAVAILABLE; @end diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementSession.m b/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementSession.m index 5ddbdcd8..3add6f13 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementSession.m +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementSession.m @@ -120,6 +120,17 @@ - (void)writeConfiguration:(YKFManagementInterfaceConfiguration*)configuration r [self writeConfiguration:configuration reboot:reboot lockCode:nil newLockCode:nil completion:completion]; } +- (void)deviceReset:(YKFManagementSessionDeviceResetCompletionBlock)completion { + if (![self.features.deviceReset isSupportedBySession:self]) { + completion([[NSError alloc] initWithDomain:YKFManagementErrorDomain code:YKFManagementErrorCodeUnsupportedOperation userInfo:@{NSLocalizedDescriptionKey: @"Device reset not supported by this YubiKey."}]); + return; + } + YKFAPDU *apdu = [[YKFAPDU alloc] initWithCla:0 ins:0x1f p1:0 p2:0 data:[NSData data] type:YKFAPDUTypeExtended]; + [self.smartCardInterface executeCommand:apdu completion:^(NSData * _Nullable data, NSError * _Nullable error) { + completion(error); + }]; +} + // No application side state that needs clearing but this will be called when another // session is replacing the YKFManagementSession. - (void)clearSessionState { diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVBioMetadata+Private.h b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVBioMetadata+Private.h new file mode 100644 index 00000000..25751dc8 --- /dev/null +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVBioMetadata+Private.h @@ -0,0 +1,25 @@ +// Copyright 2018-2024 Yubico AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#ifndef YKFPIVBioMetadata_Private_h +#define YKFPIVBioMetadata_Private_h + +#import "YKFPIVBioMetadata.h" + +@interface YKFPIVBioMetadata() + +- (instancetype)initWithIsConfigured:(bool)isConfigured attemptsRemaining:(int)attemptsRemaining temporaryPin:(bool)temporaryPin NS_DESIGNATED_INITIALIZER; + +@end + +#endif /* YKFPIVBioMetadata_Private_h */ diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVBioMetadata.h b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVBioMetadata.h new file mode 100644 index 00000000..cb31d05b --- /dev/null +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVBioMetadata.h @@ -0,0 +1,27 @@ +// Copyright 2018-2024 Yubico AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface YKFPIVBioMetadata : NSObject + +@property (nonatomic, readonly) bool isConfigured; +@property (nonatomic, readonly) int attemptsRemaining; +@property (nonatomic, readonly) bool temporaryPin; + +@end + +NS_ASSUME_NONNULL_END diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVBioMetadata.m b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVBioMetadata.m new file mode 100644 index 00000000..3c70c9b4 --- /dev/null +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVBioMetadata.m @@ -0,0 +1,38 @@ +// Copyright 2018-2024 Yubico AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "YKFPIVBioMetadata.h" + +@interface YKFPIVBioMetadata() + +@property (nonatomic, readwrite) bool isConfigured; +@property (nonatomic, readwrite) int attemptsRemaining; +@property (nonatomic, readwrite) bool temporaryPin; + +@end + +@implementation YKFPIVBioMetadata + +- (instancetype)initWithIsConfigured:(bool)isConfigured attemptsRemaining:(int)attemptsRemaining temporaryPin:(bool)temporaryPin +{ + self = [super init]; + if (self) { + self.isConfigured = isConfigured; + self.attemptsRemaining = attemptsRemaining; + self.temporaryPin = temporaryPin; + } + return self; +} + +@end diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVKeyType.h b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVKeyType.h index 070248ba..0025751d 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVKeyType.h +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVKeyType.h @@ -1,10 +1,16 @@ +// Copyright 2018-2024 Yubico AB // -// YKFPIVKeyType.h -// YubiKit +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Created by Jens Utbult on 2021-03-18. -// Copyright © 2021 Yubico. All rights reserved. +// http://www.apache.org/licenses/LICENSE-2.0 // +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. #ifndef YKFPIVKeyType_h #define YKFPIVKeyType_h diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVKeyType.m b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVKeyType.m index b8efe8b3..8707bba9 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVKeyType.m +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVKeyType.m @@ -1,10 +1,16 @@ +// Copyright 2018-2024 Yubico AB // -// YKFPIVKeyType.m -// YubiKit +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Created by Jens Utbult on 2021-03-18. -// Copyright © 2021 Yubico. All rights reserved. +// http://www.apache.org/licenses/LICENSE-2.0 // +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. #import #import "YKFPIVKeyType.h" diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVSession+Private.h b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVSession+Private.h index 0b0fb492..f027a46c 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVSession+Private.h +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVSession+Private.h @@ -1,10 +1,16 @@ +// Copyright 2018-2024 Yubico AB // -// YKFPIVSession+Private.h -// YubiKit +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Created by Jens Utbult on 2021-02-11. -// Copyright © 2021 Yubico. All rights reserved. +// http://www.apache.org/licenses/LICENSE-2.0 // +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. #ifndef YKFPIVSession_Private_h #define YKFPIVSession_Private_h diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVSession.h b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVSession.h index 264cb637..ee493ccc 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVSession.h +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVSession.h @@ -32,7 +32,9 @@ typedef NS_ENUM(NSUInteger, YKFPIVPinPolicy) { YKFPIVPinPolicyDefault = 0x0, YKFPIVPinPolicyNever = 0x1, YKFPIVPinPolicyOnce = 0x2, - YKFPIVPinPolicyAlways = 0x3 + YKFPIVPinPolicyAlways = 0x3, + YKFPIVPinPolicyMatchOnce = 0x4, + YKFPIVPinPolicyMatchAlways = 0x5 }; /// Available slots for PIV application. @@ -45,7 +47,7 @@ typedef NS_ENUM(NSUInteger, YKFPIVSlot) { }; /// PIV error domain. -extern NSString* _Nonnull const YKFPIVFErrorDomain; +extern NSString* _Nonnull const YKFPIVErrorDomain; /// PIV error codes. typedef NS_ENUM(NSUInteger, YKFPIVErrorCode) { @@ -60,7 +62,7 @@ typedef NS_ENUM(NSUInteger, YKFPIVErrorCode) { YKFPIVErrorCodeIllegalArgument = 9 }; -@class YKFPIVSessionFeatures, YKFPIVManagementKeyType, YKFPIVManagementKeyMetadata, YKFPIVSlotMetadata; +@class YKFPIVSessionFeatures, YKFPIVManagementKeyType, YKFPIVManagementKeyMetadata, YKFPIVSlotMetadata, YKFPIVBioMetadata; NS_ASSUME_NONNULL_BEGIN @@ -151,6 +153,16 @@ typedef void (^YKFPIVSessionSlotMetadataCompletionBlock) typedef void (^YKFPIVSessionManagementKeyMetadataCompletionBlock) (YKFPIVManagementKeyMetadata* _Nullable metaData, NSError* _Nullable error); +/// @abstract Response block for [getBioMetadata:completion:] which provides the bio key metadata or an error. +/// @param metaData The management key metadata. +/// @param error An error object that indicates why the request failed, or nil if the request was successful. +typedef void (^YKFPIVSessionBioMetadataCompletionBlock) + (YKFPIVBioMetadata* _Nullable metaData, NSError* _Nullable error); + +typedef void (^YKFPIVSessionBioVerifyUvCompletionBlock) + (NSData* _Nullable data, NSError* _Nullable error); + + /// @class YKFPIVSession /// @abstract Provides the interface for executing PIV requests with the key. /// @discussion The PIV session is mantained by the YKFConnection which controls its lifecycle. The application @@ -442,6 +454,31 @@ typedef void (^YKFPIVSessionManagementKeyMetadataCompletionBlock) /// @note This method is thread safe and can be invoked from any thread (main or a background thread). - (void)setPinAttempts:(int)pinAttempts pukAttempts:(int)pukAttempts completion:(nonnull YKFPIVSessionGenericCompletionBlock)completion; +/// @abstract Reads metadata specific to YubiKey Bio multi-protocol. +/// @param completion The completion handler that gets called once the YubiKey has finished processing the request. +/// This handler is executed on a background queue. +- (void)getBioMetadataWithCompletion:(nonnull YKFPIVSessionBioMetadataCompletionBlock)completion; + +/// @abstract Authenticate with YubiKey Bio multi-protocol capabilities. +/// +/// @discussion Before calling this method, clients must verify that the authenticator is bio-capable and +/// not blocked for bio matching. +/// @param requestTemporaryPin After successful match generate a temporary PIN. +/// @param checkOnly Check verification state of biometrics, don't perform UV. +/// @param completion Temporary pin if requestTemporaryPin is true, otherwise null. +- (void)verifyUvRequestTemporaryPin:(bool)requestTemporaryPin checkOnly:(bool)checkOnly completion:(nonnull YKFPIVSessionBioVerifyUvCompletionBlock)completion + NS_SWIFT_NAME(verifyUv(requestTemporaryPin:checkOnly:completion:)); + +/// @abstract Authenticate YubiKey Bio multi-protocol with temporary PIN. +/// +/// @discussion The PIN has to be generated by calling `verifyUvRequestTemporaryPin:checkOnly:completion` and is +/// valid only for operations during this session and depending on pin policy. +/// Before calling this method, clients must verify that the authenticator is bio-capable and +/// not blocked for bio matching. +/// +/// - Parameter pin: Temporary pin. +- (void)verifyTemporaryPin:(NSData *)pin completion:(nonnull YKFPIVSessionGenericCompletionBlock)completion; + /// Not available. Use only the instance from the YKFAccessoryConnection or YKFNFCConnection. - (nonnull instancetype)init NS_UNAVAILABLE; diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVSession.m b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVSession.m index 49e36701..8947bfc3 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVSession.m +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVSession.m @@ -23,13 +23,17 @@ #import "YKFFeature.h" #import "YKFPIVSessionFeatures.h" #import "YKFSessionError.h" +#import "YKFSessionError+Private.h" #import "YKFNSDataAdditions+Private.h" #import "NSArray+YKFTLVRecord.h" #import "YKFPIVManagementKeyType.h" #import "YKFAPDU+Private.h" #import "YKFPIVError.h" +#import "YKFAPDUError.h" +#import "YKFInvalidPinError.h" #import "YKFSessionError+Private.h" #import "YKFPIVSlotMetadata+Private.h" +#import "YKFPIVBioMetadata+Private.h" #import "YKFPIVManagementKeyMetadata+Private.h" #import "YKFPIVPadding+Private.h" #import "TKTLVRecordAdditions+Private.h" @@ -81,6 +85,11 @@ static const NSUInteger YKFPIVTagPinPolicy = 0xaa; static const NSUInteger YKFPIVTagTouchPolicy = 0xab; +static const NSUInteger YKFPIVTagMetadataBioConfigured = 0x07; +static const NSUInteger YKFPIVTagMetadataTemporaryPIN = 0x08; + +static const NSUInteger YKFPIVSlotOCCAuth = 0x96; + // P2 static const NSUInteger YKFPIVP2Pin = 0x80; static const NSUInteger YKFPIVP2Puk = 0x81; @@ -296,21 +305,22 @@ - (SecKeyRef)secKeyFromYubiKeyData:(NSData *)data keyType:(YKFPIVKeyType)type er } - (void)generateKeyInSlot:(YKFPIVSlot)slot type:(YKFPIVKeyType)type pinPolicy:(YKFPIVPinPolicy)pinPolicy touchPolicy:(YKFPIVTouchPolicy)touchPolicy completion:(nonnull YKFPIVSessionReadKeyCompletionBlock)completion { - NSError *error = [self checkKeySupport:type pinPolicy:pinPolicy touchPolicy:touchPolicy generateKey:YES]; - if (error) { - completion(nil, error); - return; - } - NSMutableData *data = [NSMutableData dataWithBytes:&type length:1]; - YKFTLVRecord *tlv = [[YKFTLVRecord alloc] initWithTag:YKFPIVTagGenAlgorithm value:data]; - YKFTLVRecord *tlvsContainer = [[YKFTLVRecord alloc] initWithTag:0xac value:tlv.data]; - NSData *tlvsData = tlvsContainer.data; - YKFAPDU *apdu = [[YKFAPDU alloc] initWithCla:0 ins:YKFPIVInsGenerateAsymetric p1:0 p2:slot data:tlvsData type:YKFAPDUTypeExtended]; - [self.smartCardInterface executeCommand:apdu timeout:120.0 completion:^(NSData * _Nullable data, NSError * _Nullable error) { - NSData *keyData = [[YKFTLVRecord sequenceOfRecordsFromData:data] ykfTLVRecordWithTag:(UInt64)0x7F49].value; - NSError *keyError; - SecKeyRef publicKey = [self secKeyFromYubiKeyData:keyData keyType:type error:&keyError]; - completion(publicKey, keyError); + [self checkKeySupport:type pinPolicy:pinPolicy touchPolicy:touchPolicy generateKey:YES completion:^(NSError * _Nullable error) { + if (error) { + completion(nil, error); + return; + } + NSMutableData *data = [NSMutableData dataWithBytes:&type length:1]; + YKFTLVRecord *tlv = [[YKFTLVRecord alloc] initWithTag:YKFPIVTagGenAlgorithm value:data]; + YKFTLVRecord *tlvsContainer = [[YKFTLVRecord alloc] initWithTag:0xac value:tlv.data]; + NSData *tlvsData = tlvsContainer.data; + YKFAPDU *apdu = [[YKFAPDU alloc] initWithCla:0 ins:YKFPIVInsGenerateAsymetric p1:0 p2:slot data:tlvsData type:YKFAPDUTypeExtended]; + [self.smartCardInterface executeCommand:apdu timeout:120.0 completion:^(NSData * _Nullable data, NSError * _Nullable error) { + NSData *keyData = [[YKFTLVRecord sequenceOfRecordsFromData:data] ykfTLVRecordWithTag:(UInt64)0x7F49].value; + NSError *keyError; + SecKeyRef publicKey = [self secKeyFromYubiKeyData:keyData keyType:type error:&keyError]; + completion(publicKey, keyError); + }]; }]; } @@ -318,7 +328,7 @@ - (void)generateKeyInSlot:(YKFPIVSlot)slot type:(YKFPIVKeyType)type completion:( [self generateKeyInSlot:slot type:type pinPolicy:YKFPIVPinPolicyDefault touchPolicy:YKFPIVTouchPolicyDefault completion:completion]; } -- (NSError * _Nullable)checkKeySupport:(YKFPIVKeyType)keyType pinPolicy:(YKFPIVPinPolicy)pinPolicy touchPolicy:(YKFPIVTouchPolicy)touchPolicy generateKey:(bool)generateKey { +- (void)checkKeySupport:(YKFPIVKeyType)keyType pinPolicy:(YKFPIVPinPolicy)pinPolicy touchPolicy:(YKFPIVTouchPolicy)touchPolicy generateKey:(bool)generateKey completion:(nonnull YKFPIVSessionGenericCompletionBlock)completion { NSString *errorMessage = nil; if (keyType == YKFPIVKeyTypeECCP384) { @@ -352,72 +362,86 @@ - (NSError * _Nullable)checkKeySupport:(YKFPIVKeyType)keyType pinPolicy:(YKFPIVP } if (errorMessage) { - return [[NSError alloc] initWithDomain:YKFPIVErrorDomain code:YKFPIVErrorCodeUnsupportedOperation userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ not supported by this YubiKey.", errorMessage]}]; + NSError *error = [[NSError alloc] initWithDomain:YKFPIVErrorDomain code:YKFPIVErrorCodeUnsupportedOperation userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ not supported by this YubiKey.", errorMessage]}]; + completion(error); + return; } - return nil; + if (pinPolicy == YKFPIVPinPolicyMatchAlways || pinPolicy == YKFPIVPinPolicyMatchOnce) { + [self getBioMetadataWithCompletion:^(YKFPIVBioMetadata * _Nullable metaData, NSError * _Nullable error) { + if (error) { + completion(error); + } else { + completion(nil); + } + }]; + } else { + completion(nil); + } } - (void)putKey:(SecKeyRef)key inSlot:(YKFPIVSlot)slot pinPolicy:(YKFPIVPinPolicy)pinPolicy touchPolicy:(YKFPIVTouchPolicy)touchPolicy completion:(nonnull YKFPIVSessionPutKeyCompletionBlock)completion { YKFPIVKeyType keyType = YKFPIVKeyTypeFromKey(key); - NSError *error = [self checkKeySupport:keyType pinPolicy:pinPolicy touchPolicy:touchPolicy generateKey:NO]; - if (error) { - completion(keyType, error); - } - - CFErrorRef cfError = nil; - NSData *data = (__bridge NSData*)SecKeyCopyExternalRepresentation(key, &cfError); - if (cfError) { - NSError *error = (__bridge NSError *) cfError; - completion(YKFPIVKeyTypeUnknown, error); - return; - } - NSMutableData *mutableData = [NSMutableData data]; - switch (keyType) { - case YKFPIVKeyTypeRSA1024: - case YKFPIVKeyTypeRSA2048: - case YKFPIVKeyTypeRSA3072: - case YKFPIVKeyTypeRSA4096: - { - NSArray *records = [YKFTLVRecord sequenceOfRecordsFromData:[YKFTLVRecord recordFromData:data].value]; - NSData *primeOne = records[4].value; - NSData *primeTwo = records[5].value; - NSData *exponentOne = records[6].value; - NSData *exponentTwo = records[7].value; - NSData *coefficient = records[8].value; - - int length = YKFPIVSizeFromKeyType(keyType) / 2; - [mutableData appendData:[[YKFTLVRecord alloc] initWithTag:0x01 value:[primeOne ykf_toLength:length]].data]; - [mutableData appendData:[[YKFTLVRecord alloc] initWithTag:0x02 value:[primeTwo ykf_toLength:length]].data]; - [mutableData appendData:[[YKFTLVRecord alloc] initWithTag:0x03 value:[exponentOne ykf_toLength:length]].data]; - [mutableData appendData:[[YKFTLVRecord alloc] initWithTag:0x04 value:[exponentTwo ykf_toLength:length]].data]; - [mutableData appendData:[[YKFTLVRecord alloc] initWithTag:0x05 value:[coefficient ykf_toLength:length]].data]; - break; - } - case YKFPIVKeyTypeECCP256: - case YKFPIVKeyTypeECCP384: - { - int keyLength = YKFPIVSizeFromKeyType(keyType); - NSData *privateKey = [data subdataWithRange:NSMakeRange(1 + 2 * keyLength, keyLength)]; - YKFTLVRecord *record = [[YKFTLVRecord alloc] initWithTag:0x06 value:privateKey]; - [mutableData appendData:record.data]; - break; + [self checkKeySupport:keyType pinPolicy:pinPolicy touchPolicy:touchPolicy generateKey:NO completion:^(NSError * _Nullable error) { + if (error) { + completion(keyType, error); + return; } - default: - completion(YKFPIVKeyTypeUnknown, [[NSError alloc] initWithDomain:YKFPIVErrorDomain code:YKFPIVErrorCodeUnknownKeyType userInfo:@{NSLocalizedDescriptionKey: @"Unknown key type."}]); + + CFErrorRef cfError = nil; + NSData *data = (__bridge NSData*)SecKeyCopyExternalRepresentation(key, &cfError); + if (cfError) { + NSError *error = (__bridge NSError *) cfError; + completion(YKFPIVKeyTypeUnknown, error); return; - } - - if (pinPolicy != YKFPIVPinPolicyDefault) { - [mutableData appendData:[[YKFTLVRecord alloc] initWithTag:YKFPIVTagPinPolicy value:[NSData dataWithBytes:&pinPolicy length:1]].value]; - } - if (touchPolicy != YKFPIVTouchPolicyDefault) { - [mutableData appendData:[[YKFTLVRecord alloc] initWithTag:YKFPIVTagTouchPolicy value:[NSData dataWithBytes:&touchPolicy length:1]].value]; - } - - YKFAPDU *apdu = [[YKFAPDU alloc] initWithCla:0 ins:YKFPIVInsImportKey p1:keyType p2:slot data:mutableData type:YKFAPDUTypeExtended]; - [self.smartCardInterface executeCommand:apdu completion:^(NSData * _Nullable data, NSError * _Nullable error) { - completion(keyType, error); + } + NSMutableData *mutableData = [NSMutableData data]; + switch (keyType) { + case YKFPIVKeyTypeRSA1024: + case YKFPIVKeyTypeRSA2048: + case YKFPIVKeyTypeRSA3072: + case YKFPIVKeyTypeRSA4096: + { + NSArray *records = [YKFTLVRecord sequenceOfRecordsFromData:[YKFTLVRecord recordFromData:data].value]; + NSData *primeOne = records[4].value; + NSData *primeTwo = records[5].value; + NSData *exponentOne = records[6].value; + NSData *exponentTwo = records[7].value; + NSData *coefficient = records[8].value; + + int length = YKFPIVSizeFromKeyType(keyType) / 2; + [mutableData appendData:[[YKFTLVRecord alloc] initWithTag:0x01 value:[primeOne ykf_toLength:length]].data]; + [mutableData appendData:[[YKFTLVRecord alloc] initWithTag:0x02 value:[primeTwo ykf_toLength:length]].data]; + [mutableData appendData:[[YKFTLVRecord alloc] initWithTag:0x03 value:[exponentOne ykf_toLength:length]].data]; + [mutableData appendData:[[YKFTLVRecord alloc] initWithTag:0x04 value:[exponentTwo ykf_toLength:length]].data]; + [mutableData appendData:[[YKFTLVRecord alloc] initWithTag:0x05 value:[coefficient ykf_toLength:length]].data]; + break; + } + case YKFPIVKeyTypeECCP256: + case YKFPIVKeyTypeECCP384: + { + int keyLength = YKFPIVSizeFromKeyType(keyType); + NSData *privateKey = [data subdataWithRange:NSMakeRange(1 + 2 * keyLength, keyLength)]; + YKFTLVRecord *record = [[YKFTLVRecord alloc] initWithTag:0x06 value:privateKey]; + [mutableData appendData:record.data]; + break; + } + default: + completion(YKFPIVKeyTypeUnknown, [[NSError alloc] initWithDomain:YKFPIVErrorDomain code:YKFPIVErrorCodeUnknownKeyType userInfo:@{NSLocalizedDescriptionKey: @"Unknown key type."}]); + return; + } + + if (pinPolicy != YKFPIVPinPolicyDefault) { + [mutableData appendData:[[YKFTLVRecord alloc] initWithTag:YKFPIVTagPinPolicy value:[NSData dataWithBytes:&pinPolicy length:1]].value]; + } + if (touchPolicy != YKFPIVTouchPolicyDefault) { + [mutableData appendData:[[YKFTLVRecord alloc] initWithTag:YKFPIVTagTouchPolicy value:[NSData dataWithBytes:&touchPolicy length:1]].value]; + } + + YKFAPDU *apdu = [[YKFAPDU alloc] initWithCla:0 ins:YKFPIVInsImportKey p1:keyType p2:slot data:mutableData type:YKFAPDUTypeExtended]; + [self.smartCardInterface executeCommand:apdu completion:^(NSData * _Nullable data, NSError * _Nullable error) { + completion(keyType, error); + }]; }]; } @@ -857,6 +881,88 @@ - (void)blockPuk:(int)counter completion:(YKFPIVSessionGenericCompletionBlock)co }]; } +- (void)getBioMetadataWithCompletion:(nonnull YKFPIVSessionBioMetadataCompletionBlock)completion { + YKFAPDU *apdu = [[YKFAPDU alloc] initWithCla:0x00 ins: YKFPIVInsGetMetadata p1:0x00 p2:YKFPIVSlotOCCAuth data:[NSData data] type:YKFAPDUTypeShort]; + [self.smartCardInterface executeCommand:apdu completion:^(NSData * _Nullable data, NSError * _Nullable error) { + if (error) { + if (error.domain == YKFSessionErrorDomain && (error.code == YKFAPDUErrorCodeReferencedDataNotFound || error.code == YKFAPDUErrorCodeInsNotSupported)) { + completion(nil, [[NSError alloc] initWithDomain:YKFPIVErrorDomain code:YKFPIVErrorCodeUnsupportedOperation userInfo:@{NSLocalizedDescriptionKey: @"Get bio metadata not supported by this YubiKey."}]); + } else { + completion(nil, error); + } + return; + } + NSArray *records = [YKFTLVRecord sequenceOfRecordsFromData:data]; + bool isConfigured = [records ykfTLVRecordWithTag:YKFPIVTagMetadataBioConfigured].value.ykf_integerValue; + bool temporaryPin = [records ykfTLVRecordWithTag:YKFPIVTagMetadataTemporaryPIN].value.ykf_integerValue; + int retries = (int)[records ykfTLVRecordWithTag:YKFPIVTagMetadataRetries].value.ykf_integerValue; + YKFPIVBioMetadata *metadata = [[YKFPIVBioMetadata alloc] initWithIsConfigured:isConfigured attemptsRemaining:retries temporaryPin:temporaryPin]; + completion(metadata, nil); + }]; +} + +- (void)verifyUvRequestTemporaryPin:(bool)requestTemporaryPin checkOnly:(bool)checkOnly completion:(nonnull YKFPIVSessionBioVerifyUvCompletionBlock)completion { + if (requestTemporaryPin && checkOnly) { + completion(nil, [[NSError alloc] initWithDomain:YKFPIVErrorDomain code:YKFPIVErrorCodeIllegalArgument userInfo:@{NSLocalizedDescriptionKey: @"It's not possible to request a temporary pin and do a check only."}]); + } + + NSData *temporaryPinData = nil; + if (!checkOnly) { + if (requestTemporaryPin) { + temporaryPinData = [[[YKFTLVRecord alloc] initWithTag:0x02 value:[NSData data]] data]; + } else { + temporaryPinData = [[[YKFTLVRecord alloc] initWithTag:0x03 value:[NSData data]] data]; + } + } + YKFAPDU *apdu = [[YKFAPDU alloc] initWithCla:0x00 ins: YKFPIVInsVerify p1:0x00 p2:YKFPIVSlotOCCAuth data:temporaryPinData type:YKFAPDUTypeExtended]; + [self.smartCardInterface executeCommand:apdu completion:^(NSData * _Nullable data, NSError * _Nullable error) { + if (error) { + if (error.domain == YKFSessionErrorDomain && error.code == YKFAPDUErrorCodeReferencedDataNotFound) { + completion(nil, [[NSError alloc] initWithDomain:YKFPIVErrorDomain code:YKFPIVErrorCodeUnsupportedOperation userInfo:@{NSLocalizedDescriptionKey: @"Verify uv not supported by this YubiKey."}]); + return; + } + int retries = [self getRetriesFromStatusCode:(int)error.code]; + if (retries >= 0) { + completion(nil, [YKFInvalidPinError invalidPinErrorWithRetries:retries]); + return; + } else { + completion(nil, error); + return; + } + } + if (requestTemporaryPin) { + completion(data, nil); + } else { + completion(nil, nil); + } + }]; +} + +- (void)verifyTemporaryPin:(NSData *)pinData completion:(nonnull YKFPIVSessionGenericCompletionBlock)completion { + if (pinData.length != 16) { + completion([[NSError alloc] initWithDomain:YKFPIVErrorDomain code:YKFPIVErrorCodeIllegalArgument userInfo:@{NSLocalizedDescriptionKey: @"Pin data length should be 16 bytes."}]); + return; + } + NSData *data = [[[YKFTLVRecord alloc] initWithTag:0x01 value:pinData] data]; + YKFAPDU *apdu = [[YKFAPDU alloc] initWithCla:0 ins:YKFPIVInsVerify p1:0 p2:YKFPIVSlotOCCAuth data:data type:YKFAPDUTypeShort]; + [self.smartCardInterface executeCommand:apdu completion:^(NSData * _Nullable data, NSError * _Nullable error) { + if (error == nil) { + completion(nil); + return; + } else { + if (error.domain == YKFSessionErrorDomain && error.code == YKFAPDUErrorCodeReferencedDataNotFound) { + completion([[NSError alloc] initWithDomain:YKFPIVErrorDomain code:YKFPIVErrorCodeUnsupportedOperation userInfo:@{NSLocalizedDescriptionKey: @"Verify uv not supported by this YubiKey."}]); + return; + } + if (error.code == 0x63c0) { + completion([YKFInvalidPinError invalidPinErrorWithRetries:0]); + } else { + completion(error); + } + } + }]; +} + - (void)changeReference:(UInt8)ins p2:(UInt8)p2 valueOne:(NSString *)valueOne valueTwo:(NSString *)valueTwo completion:(nonnull YKFPIVSessionVerifyPinCompletionBlock)completion { NSMutableData *data = [self paddedDataWithPin:valueOne].mutableCopy; [data appendData:[self paddedDataWithPin:valueTwo]]; diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementSessionFeatures.h b/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementSessionFeatures.h index 7ee87835..4aef5318 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementSessionFeatures.h +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementSessionFeatures.h @@ -14,6 +14,7 @@ @interface YKFManagementSessionFeatures : NSObject @property (nonatomic, readonly) YKFFeature * _Nonnull deviceInfo; @property (nonatomic, readonly) YKFFeature * _Nonnull deviceConfig; +@property (nonatomic, readonly) YKFFeature * _Nonnull deviceReset; @end #endif /* YKFManagementSessionFeatures_h */ diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementSessionFeatures.m b/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementSessionFeatures.m index 957a4b48..7b939e02 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementSessionFeatures.m +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementSessionFeatures.m @@ -19,6 +19,7 @@ @interface YKFManagementSessionFeatures() @property (nonatomic, readwrite) YKFFeature * _Nonnull deviceInfo; @property (nonatomic, readwrite) YKFFeature * _Nonnull deviceConfig; +@property (nonatomic, readwrite) YKFFeature * _Nonnull deviceReset; @end @implementation YKFManagementSessionFeatures @@ -28,6 +29,7 @@ - (instancetype)init { if (self) { self.deviceInfo = [[YKFFeature alloc] initWithName:@"Device info" versionString:@"4.1.0"]; self.deviceConfig = [[YKFFeature alloc] initWithName:@"Device config" versionString:@"5.0.0"]; + self.deviceReset = [[YKFFeature alloc] initWithName:@"Device reset" versionString:@"5.6.0"]; } return self; } diff --git a/YubiKit/YubiKit/YubiKit.h b/YubiKit/YubiKit/YubiKit.h index 7c5149c6..538d7706 100644 --- a/YubiKit/YubiKit/YubiKit.h +++ b/YubiKit/YubiKit/YubiKit.h @@ -57,9 +57,12 @@ #import "YKFManagementInterfaceConfiguration.h" #import "YKFPIVKeyType.h" #import "YKFPIVSlotMetadata.h" +#import "YKFPIVBioMetadata.h" #import "YKFChallengeResponseSession.h" #import "YKFManagementSession.h" +#import "YKFInvalidPinError.h" + #import "YKFSlot.h" #import "YKFFIDO2MakeCredentialResponse.h" diff --git a/YubiKitTests/Tests/ManagementTests.swift b/YubiKitTests/Tests/ManagementTests.swift index 4c06c22c..ca9a2312 100644 --- a/YubiKitTests/Tests/ManagementTests.swift +++ b/YubiKitTests/Tests/ManagementTests.swift @@ -158,6 +158,53 @@ stmVersion: \(String(describing: deviceInfo.stmVersion)) } } } + + func testBioDeviceReset() throws { + runYubiKitTest { connection, completion in + connection.managementSession { session, error in + guard let session else { XCTFail("Failed to get Management Session: \(error!)"); return } + session.getDeviceInfo { info, error in + guard let info else { XCTFail("Failed to get device info: \(error!)"); return } + guard info.formFactor == .usbaBio || info.formFactor == .usbcBio else { + print("⚠️ Skip testBioDeviceReset()") + completion() + return + } + session.deviceReset { error in + XCTAssertNil(error) + connection.pivSession { session, error in + guard let session else { XCTFail("Failed to get PIV Session: \(error!)"); return } + session.getPinMetadata { isDefault, _, _, error in + XCTAssertNil(error) + XCTAssertTrue(isDefault) + session.setPin("654321", oldPin: "123456") { error in + XCTAssertNil(error) + session.getPinMetadata { isDefault, _, _, error in + XCTAssertNil(error) + XCTAssertFalse(isDefault) + connection.managementSession { session, error in + guard let session else { XCTFail("Failed to get Management Session: \(error!)"); return } + session.deviceReset { error in + XCTAssertNil(error) + connection.pivSession { session, error in + guard let session else { XCTFail("Failed to get PIV Session: \(error!)"); return } + session.getPinMetadata { isDefault, _, _, error in + XCTAssertNil(error) + XCTAssertTrue(isDefault) + completion() + } + } + } + } + } + } + } + } + } + } + } + } + } } extension YKFConnectionProtocol { diff --git a/YubiKitTests/Tests/PIVTests.swift b/YubiKitTests/Tests/PIVTests.swift index f54f16a3..ba72aa12 100644 --- a/YubiKitTests/Tests/PIVTests.swift +++ b/YubiKitTests/Tests/PIVTests.swift @@ -836,6 +836,81 @@ class PIVTests: XCTestCase { } } } + + // This will test auth on a YubiKey Bio. To run the test at least one fingerprint needs to be registered. + func testBioAuthentication() throws { + runYubiKitTest { connection, completion in + connection.managementSession { session, error in + guard let session else { XCTFail("Failed to get Management Session: \(error!)"); return } + session.getDeviceInfo { info, error in + guard let info else { XCTFail("Failed to get device info: \(error!)"); return } + guard info.formFactor == .usbaBio || info.formFactor == .usbcBio else { + print("⚠️ Skip testBioAuthentication()"); + completion(); + return; + } + connection.pivSession { session, error in + guard let session else { XCTFail("Failed to get PIV Session: \(error!)"); return } + session.getBioMetadata { metadata, error in + guard let metadata else { XCTFail("Failed to get Bio metadata: \(error!)"); return } + guard metadata.isConfigured else { + let message = "No fingerprints registered for this yubikey or there's an error in getBioMetadata()." + print("⚠️ \(message)") + XCTFail(message) + return + } + XCTAssertTrue(metadata.attemptsRemaining > 0) + session.verifyUv(requestTemporaryPin: false, checkOnly: false) { pin, error in + XCTAssertNil(error) + XCTAssertNil(pin) + session.verifyUv(requestTemporaryPin: true, checkOnly: false) { pin, error in + guard let pin else { XCTFail("Failed to get temporary pin: \(error!)"); return } + XCTAssertNil(error) + session.getBioMetadata { metadata , error in + guard let metadata else { XCTFail("Failed to get Bio metadata: \(error!)"); return } + XCTAssertTrue(metadata.temporaryPin) + session.verifyTemporaryPin(pin) { error in + XCTAssertNil(error) + completion() + } + } + } + } + } + } + } + } + } + } + + func testBioPinPolicyErrorOnNonBioKey() throws { + runYubiKitTest { connection, completion in + connection.managementSession { session, error in + guard let session else { XCTFail("Failed to get Management Session: \(error!)"); return } + session.getDeviceInfo { info, error in + guard let info else { XCTFail("Failed to get device info: \(error!)"); return } + guard info.formFactor != .usbaBio && info.formFactor != .usbcBio else { + print("⚠️ Skip testBioPinPolicyErrorOnNonBioKey() since this is a bio key.") + completion(); + return; + } + connection.authenticatedPivTestSession { session in + session.generateKey(in: .signature, type: .ECCP256, pinPolicy: .matchOnce, touchPolicy: .default) { key, error in + guard let error = error as? NSError else { XCTFail("Failed to return error."); completion(); return; } + XCTAssertEqual(error.domain, YKFPIVErrorDomain) + XCTAssertEqual(error.code, Int(YKFPIVErrorCode.unsupportedOperation.rawValue)) + session.generateKey(in: .signature, type: .ECCP256, pinPolicy: .matchAlways, touchPolicy: .default) { key, error in + guard let error = error as? NSError else { XCTFail("Failed to return error."); completion(); return; } + XCTAssertEqual(error.domain, YKFPIVErrorDomain) + XCTAssertEqual(error.code, Int(YKFPIVErrorCode.unsupportedOperation.rawValue)) + completion() + } + } + } + } + } + } + } } extension YKFPIVSession {