diff --git a/YubiKit/YubiKit/Connections/Shared/APDU/FIDO2/YKFFIDO2GetAssertionAPDU.h b/YubiKit/YubiKit/Connections/Shared/APDU/FIDO2/YKFFIDO2GetAssertionAPDU.h index a1559b88..de3b682c 100644 --- a/YubiKit/YubiKit/Connections/Shared/APDU/FIDO2/YKFFIDO2GetAssertionAPDU.h +++ b/YubiKit/YubiKit/Connections/Shared/APDU/FIDO2/YKFFIDO2GetAssertionAPDU.h @@ -26,7 +26,16 @@ NS_ASSUME_NONNULL_BEGIN allowList:(NSArray * _Nullable)allowList pinAuth:(NSData * _Nullable)pinAuth pinProtocol:(NSUInteger)pinProtocol + options:(NSDictionary * _Nullable)options; + +- (nullable instancetype)initWithClientDataHash:(NSData *)clientDataHash + rpId:(NSString *)rpId + allowList:(NSArray * _Nullable)allowList + pinAuth:(NSData * _Nullable)pinAuth + pinProtocol:(NSUInteger)pinProtocol + extensions:(NSDictionary * _Nullable)extensions options:(NSDictionary * _Nullable)options NS_DESIGNATED_INITIALIZER; + //- (nullable instancetype)initWithRequest:(YKFFIDO2GetAssertionRequest *)request NS_DESIGNATED_INITIALIZER; - (instancetype)init NS_UNAVAILABLE; diff --git a/YubiKit/YubiKit/Connections/Shared/APDU/FIDO2/YKFFIDO2GetAssertionAPDU.m b/YubiKit/YubiKit/Connections/Shared/APDU/FIDO2/YKFFIDO2GetAssertionAPDU.m index b9aa5a02..16f5c5e1 100644 --- a/YubiKit/YubiKit/Connections/Shared/APDU/FIDO2/YKFFIDO2GetAssertionAPDU.m +++ b/YubiKit/YubiKit/Connections/Shared/APDU/FIDO2/YKFFIDO2GetAssertionAPDU.m @@ -37,6 +37,16 @@ - (nullable instancetype)initWithClientDataHash:(NSData *)clientDataHash pinAuth:(NSData * _Nullable)pinAuth pinProtocol:(NSUInteger)pinProtocol options:(NSDictionary * _Nullable)options { + return [self initWithClientDataHash:clientDataHash rpId:rpId allowList:allowList pinAuth:pinAuth pinProtocol:pinProtocol extensions:nil options:options]; +} + +- (nullable instancetype)initWithClientDataHash:(NSData *)clientDataHash + rpId:(NSString *)rpId + allowList:(NSArray * _Nullable)allowList + pinAuth:(NSData * _Nullable)pinAuth + pinProtocol:(NSUInteger)pinProtocol + extensions:(NSDictionary * _Nullable)extensions + options:(NSDictionary * _Nullable)options { YKFAssertAbortInit(clientDataHash); YKFAssertAbortInit(rpId); @@ -68,6 +78,11 @@ - (nullable instancetype)initWithClientDataHash:(NSData *)clientDataHash requestDictionary[YKFCBORInteger(YKFFIDO2GetAssertionAPDUKeyOptions)] = YKFCBORMap(mutableOptions); } + // Extensions + if (extensions.count > 0) { + requestDictionary[YKFCBORInteger(YKFFIDO2GetAssertionAPDUKeyExtensions)] = YKFCBORMap(extensions); + } + // Pin Auth if (pinAuth) { requestDictionary[YKFCBORInteger(YKFFIDO2GetAssertionAPDUKeyPinAuth)] = YKFCBORByteString(pinAuth); diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.h b/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.h index bd6e2189..031587d3 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.h +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.h @@ -453,6 +453,13 @@ typedef NS_ENUM(NSUInteger, YKFFIDO2SessionKeyState) { options:(NSDictionary * _Nullable)options completion:(YKFFIDO2SessionGetAssertionCompletionBlock)completion; +- (void)getAssertionWithClientDataHash:(NSData *)clientDataHash + rpId:(NSString *)rpId + allowList:(NSArray * _Nullable)allowList + options:(NSDictionary * _Nullable)options + extensions:(NSDictionary * _Nullable)extensions + completion:(YKFFIDO2SessionGetAssertionCompletionBlock)completion; + /*! @method getNextAssertionWithCompletion:completion: diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.m b/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.m index ba2cb85f..4a5f9945 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.m +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.m @@ -427,6 +427,15 @@ - (void)getAssertionWithClientDataHash:(NSData *)clientDataHash allowList:(NSArray * _Nullable)allowList options:(NSDictionary * _Nullable)options completion:(YKFFIDO2SessionGetAssertionCompletionBlock)completion { + [self getAssertionWithClientDataHash:clientDataHash rpId:rpId allowList:allowList options:options extensions:nil completion:completion]; +} + +- (void)getAssertionWithClientDataHash:(NSData *)clientDataHash + rpId:(NSString *)rpId + allowList:(NSArray * _Nullable)allowList + options:(NSDictionary * _Nullable)options + extensions:(NSDictionary * _Nullable)extensions + completion:(YKFFIDO2SessionGetAssertionCompletionBlock)completion { YKFParameterAssertReturn(clientDataHash); YKFParameterAssertReturn(rpId); YKFParameterAssertReturn(completion); @@ -444,6 +453,58 @@ - (void)getAssertionWithClientDataHash:(NSData *)clientDataHash } } + // Extensions, client authenticator input + if (extensions && extensions[@"prf"] && extensions[@"prf"][@"eval"]) { + NSString *base64EncodedFirst = extensions[@"prf"][@"eval"][@"first"]; + NSString *base64EncodedSecond = extensions[@"prf"][@"eval"][@"second"]; + + NSData *first = [[[NSData alloc] initWithBase64EncodedString:base64EncodedFirst options:0] ykf_prfSaltData]; + NSData *second = [[[NSData alloc] initWithBase64EncodedString:base64EncodedSecond options:0] ykf_prfSaltData]; + + if (first.length != 32 || (second && second.length != 32)) { + [NSException raise:@"Invalid input" format:@"Salt is not 32 bytes long."]; + } + [self executeGetSharedSecretWithCompletion:^(NSData * _Nullable sharedSecret, YKFCBORMap * _Nullable cosePlatformPublicKey, NSError * _Nullable error) { + NSMutableData *salts = [NSMutableData new]; + [salts appendData:first]; + if (second) { + [salts appendData:second]; + } + + NSData *saltEnc = [salts ykf_aes256EncryptedDataWithKey:sharedSecret]; + NSData *saltAuth = [saltEnc ykf_fido2HMACWithKey:sharedSecret]; + NSMutableDictionary *hmacSecretInput = [NSMutableDictionary new]; + hmacSecretInput[YKFCBORInteger(1)] = cosePlatformPublicKey; + hmacSecretInput[YKFCBORInteger(2)] = YKFCBORByteString(saltEnc); + hmacSecretInput[YKFCBORInteger(3)] = YKFCBORByteString([saltAuth subdataWithRange:NSMakeRange(0, 16)]); + hmacSecretInput[YKFCBORInteger(4)] = YKFCBORInteger(1); // pin uv auth protocol version + NSMutableDictionary *authenticatorInputs = [NSMutableDictionary new]; + authenticatorInputs[YKFCBORTextString(@"hmac-secret")] = YKFCBORMap(hmacSecretInput); + + YKFFIDO2GetAssertionAPDU *apdu = [[YKFFIDO2GetAssertionAPDU alloc] initWithClientDataHash:clientDataHash + rpId:rpId + allowList:allowList + pinAuth:pinAuth + pinProtocol:pinProtocol + extensions:authenticatorInputs + options:options]; + ykf_weak_self(); + [self executeFIDO2Command:apdu retryCount:0 completion:^(NSData * _Nullable data, NSError * _Nullable error) { + ykf_safe_strong_self(); + NSLog(@"%@", data.ykf_hexadecimalString); + NSData *cborData = [strongSelf cborFromKeyResponseData:data]; + YKFFIDO2GetAssertionResponse *getAssertionResponse = [[YKFFIDO2GetAssertionResponse alloc] initWithCBORData:cborData]; + + if (getAssertionResponse) { + completion(getAssertionResponse, nil); + } else { + completion(nil, [YKFFIDO2Error errorWithCode:YKFFIDO2ErrorCodeINVALID_CBOR]); + } + }]; + }]; + return; + } + YKFFIDO2GetAssertionAPDU *apdu = [[YKFFIDO2GetAssertionAPDU alloc] initWithClientDataHash:clientDataHash rpId:rpId allowList:allowList @@ -676,4 +737,6 @@ - (void)handleTouchRequired:(YKFAPDU *)apdu retryCount:(int)retryCount completi }); } + + @end diff --git a/YubiKit/YubiKit/Helpers/Additions/YKFNSDataAdditions+Private.h b/YubiKit/YubiKit/Helpers/Additions/YKFNSDataAdditions+Private.h index b415a13c..b8eca882 100644 --- a/YubiKit/YubiKit/Helpers/Additions/YKFNSDataAdditions+Private.h +++ b/YubiKit/YubiKit/Helpers/Additions/YKFNSDataAdditions+Private.h @@ -41,6 +41,8 @@ NS_ASSUME_NONNULL_BEGIN - (nullable NSData *)ykf_fido2PaddedPinData; +- (NSData *)ykf_prfSaltData; + @end @interface NSData (NSDATA_PIVAdditions) diff --git a/YubiKit/YubiKit/Helpers/Additions/YKFNSDataAdditions.m b/YubiKit/YubiKit/Helpers/Additions/YKFNSDataAdditions.m index 201648f7..628c3dfa 100644 --- a/YubiKit/YubiKit/Helpers/Additions/YKFNSDataAdditions.m +++ b/YubiKit/YubiKit/Helpers/Additions/YKFNSDataAdditions.m @@ -176,6 +176,15 @@ - (NSData *)ykf_fido2PaddedPinData { return [mutableData copy]; } +- (NSData *)ykf_prfSaltData { + NSMutableData *mutableData = [NSMutableData new]; + [mutableData appendData:[@"WebAuthn PRF" dataUsingEncoding: NSUTF8StringEncoding]]; + UInt8 padding = 0x00; + [mutableData appendBytes:&padding length:1]; + [mutableData appendData:self]; + return [mutableData ykf_SHA256]; +} + @end #pragma mark - PIV diff --git a/YubiKitTests/Tests/FIDO2Tests.swift b/YubiKitTests/Tests/FIDO2Tests.swift index 04d0d85b..1ce3107f 100644 --- a/YubiKitTests/Tests/FIDO2Tests.swift +++ b/YubiKitTests/Tests/FIDO2Tests.swift @@ -251,11 +251,13 @@ class FIDO2Tests: XCTestCase { connection.fido2TestSession { session in session.verifyPin("123456") { error in if let error { XCTFail("verifyPin failed with: \(error)"); return } - let extensions = ["prf" : true] - session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmEdDSA, options: [YKFFIDO2OptionRK: true], extensions: extensions) { response in + let createExtensions = ["prf" : []] + session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmES256, options: [YKFFIDO2OptionRK: true], extensions: createExtensions) { response in print(response.authenticatorData) print("✅ Created new FIDO2 credential: \(response)") - session.getAssertionAndAssert(response: response, options: [YKFFIDO2OptionUP: true]) { response in + + let assertExtensions = ["prf" : ["eval" : ["first" : "abba", "second" : "bebe"]]] + session.getAssertionAndAssert(response: response, options: [YKFFIDO2OptionUP: true], extensions: assertExtensions) { response in print("✅ Asserted FIDO2 credential: \(response)") completion() } @@ -335,14 +337,14 @@ extension YKFFIDO2Session { makeCredential(withClientDataHash: data, rp: rp, user: user, pubKeyCredParams: pubKeyCredParams, excludeList: nil, options: options, extensions: extensions, completion: completion) } - func getAssertionAndAssert(response: YKFFIDO2MakeCredentialResponse, options: [String: Any]? = nil, completion: @escaping (_ response: YKFFIDO2GetAssertionResponse) -> Void) { - getAssertion(response: response, options: options) { response, error in + func getAssertionAndAssert(response: YKFFIDO2MakeCredentialResponse, options: [String: Any]? = nil, extensions: [String: Any]? = nil, completion: @escaping (_ response: YKFFIDO2GetAssertionResponse) -> Void) { + getAssertion(response: response, options: options, extensions: extensions) { response, error in guard let response = response else { XCTAssertTrue(false, "🔴 Failed asserting FIDO2 credential: \(error!)"); return } completion(response) } } - func getAssertion(response: YKFFIDO2MakeCredentialResponse, options: [String: Any]? = nil, completion: @escaping YKFFIDO2SessionGetAssertionCompletionBlock) { + func getAssertion(response: YKFFIDO2MakeCredentialResponse, options: [String: Any]? = nil, extensions: [String: Any]? = nil, completion: @escaping YKFFIDO2SessionGetAssertionCompletionBlock) { let data = Data(repeating: 0, count: 32) let credentialDescriptor = YKFFIDO2PublicKeyCredentialDescriptor() credentialDescriptor.credentialId = response.authenticatorData!.credentialId! @@ -350,7 +352,7 @@ extension YKFFIDO2Session { credType.name = "public-key" credentialDescriptor.credentialType = credType let allowList = [credentialDescriptor] - getAssertionWithClientDataHash(data, rpId: "yubikit-test.com", allowList: allowList, options: options, completion: completion) + getAssertionWithClientDataHash(data, rpId: "yubikit-test.com", allowList: allowList, options: options, extensions: extensions, completion: completion) } }