diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.m b/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.m index 8b2d26c4..748aa3e0 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.m +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.m @@ -500,33 +500,55 @@ - (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."]; - } + if (extensions) { [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); - + if (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."]; + } + 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 + authenticatorInputs[YKFCBORTextString(@"hmac-secret")] = YKFCBORMap(hmacSecretInput); + } + if (extensions[@"sign"] && extensions[@"sign"][@"sign"]) { + NSString *phDataString = extensions[@"sign"][@"sign"][@"phData"]; + NSData *phData = [[NSData alloc] ykf_initWithWebsafeBase64EncodedString:phDataString dataLength:phDataString.length]; + NSDictionary *keyHandleDict = extensions[@"sign"][@"sign"][@"keyHandleByCredential"]; + NSMutableDictionary *decodedKeyHandleDict = [NSMutableDictionary new]; + for (id key in keyHandleDict) { + decodedKeyHandleDict[key] = [[NSData alloc] ykf_initWithWebsafeBase64EncodedString:keyHandleDict[key] dataLength:((NSString *)keyHandleDict[key]).length]; + } + if (allowList.count == 0) { + [NSException raise:@"Invalid input" format:@"Allow list can not be empty."]; + } + NSString *credentialId = [((YKFFIDO2PublicKeyCredentialDescriptor *) allowList.firstObject).credentialId ykf_websafeBase64EncodedString]; + + NSData *keyHandle = decodedKeyHandleDict[credentialId]; + + NSMutableDictionary *output = [NSMutableDictionary new]; + output[YKFCBORInteger(0)] = YKFCBORByteString(phData); + output[YKFCBORInteger(5)] = YKFCBORArray(@[YKFCBORByteString(keyHandle)]); + authenticatorInputs[YKFCBORTextString(@"sign")] = YKFCBORMap(output); + } + YKFFIDO2GetAssertionAPDU *apdu = [[YKFFIDO2GetAssertionAPDU alloc] initWithClientDataHash:clientDataHash rpId:rpId allowList:allowList diff --git a/YubiKitTests/Tests/FIDO2Tests.swift b/YubiKitTests/Tests/FIDO2Tests.swift index 32d0400b..5f101a16 100644 --- a/YubiKitTests/Tests/FIDO2Tests.swift +++ b/YubiKitTests/Tests/FIDO2Tests.swift @@ -69,7 +69,7 @@ class FIDO2Tests: XCTestCase { connection.fido2TestSession { session in session.setPin("123456") { _ in session.verifyPin("123456") { error in - session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmES256, options: [YKFFIDO2OptionRK: false]) { response in + session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmES256, options: [YKFFIDO2OptionRK: false]) { response, _ in print("✅ Created new FIDO2 credential: \(response)") session.getAssertionAndAssert(response: response, options: [YKFFIDO2OptionUP: true]) { response in // https://www.w3.org/TR/webauthn/#authenticator-data @@ -98,7 +98,7 @@ class FIDO2Tests: XCTestCase { connection.fido2TestSession { session in session.setPin("123456") { _ in session.verifyPin("123456") { error in - session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmES256, options: [YKFFIDO2OptionRK: false]) { response in + session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmES256, options: [YKFFIDO2OptionRK: false]) { response, _ in session.clearUserVerification() session.getAssertion(response: response, options: [YKFFIDO2OptionUP: true]) { response, error in if let response = response { @@ -171,7 +171,7 @@ class FIDO2Tests: XCTestCase { func testCreateECCNonRKCredential() { runYubiKitTest { connection, completion in connection.fido2TestSession { session in - session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmES256, options: [YKFFIDO2OptionRK: false]) { response in + session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmES256, options: [YKFFIDO2OptionRK: false]) { response, _ in print("✅ New FIDO2 credential: \(response)") completion() } @@ -182,7 +182,7 @@ class FIDO2Tests: XCTestCase { func testCreateEdSANonRKCredential() { runYubiKitTest { connection, completion in connection.fido2TestSession { session in - session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmEdDSA, options: [YKFFIDO2OptionRK: false]) { response in + session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmEdDSA, options: [YKFFIDO2OptionRK: false]) { response, _ in print("✅ Created new FIDO2 credential: \(response)") completion() } @@ -193,7 +193,7 @@ class FIDO2Tests: XCTestCase { func testCreateECCNonRKCredentialAndAssert() { runYubiKitTest { connection, completion in connection.fido2TestSession { session in - session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmES256, options: [YKFFIDO2OptionRK: false]) { response in + session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmES256, options: [YKFFIDO2OptionRK: false]) { response, _ in print("✅ Created new FIDO2 credential: \(response)") session.getAssertionAndAssert(response: response, options: [YKFFIDO2OptionUP: true]) { response in print("✅ Asserted FIDO2 credential: \(response)") @@ -207,7 +207,7 @@ class FIDO2Tests: XCTestCase { func testCreateEdDSANonRKCredentialAndAssert() { runYubiKitTest { connection, completion in connection.fido2TestSession { session in - session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmEdDSA, options: [YKFFIDO2OptionRK: false]) { response in + session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmEdDSA, options: [YKFFIDO2OptionRK: false]) { response, _ in print("✅ Created new FIDO2 credential: \(response)") session.getAssertionAndAssert(response: response, options: [YKFFIDO2OptionUP: true]) { response in print("✅ Asserted FIDO2 credential: \(response)") @@ -221,7 +221,7 @@ class FIDO2Tests: XCTestCase { func testCreateEdDSARKCredentialAndAssert() { runYubiKitTest { connection, completion in connection.fido2TestSession { session in - session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmEdDSA, options: [YKFFIDO2OptionRK: true]) { response in + session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmEdDSA, options: [YKFFIDO2OptionRK: true]) { response, _ in print("✅ Created new FIDO2 credential: \(response)") session.getAssertionAndAssert(response: response, options: [YKFFIDO2OptionUP: true]) { response in print("✅ Asserted FIDO2 credential: \(response)") @@ -235,7 +235,7 @@ class FIDO2Tests: XCTestCase { func testCreateEdDSARKCredentialAndAssertWithoutUP() { runYubiKitTest { connection, completion in connection.fido2TestSession { session in - session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmEdDSA, options: [YKFFIDO2OptionRK: true]) { response in + session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmEdDSA, options: [YKFFIDO2OptionRK: true]) { response, _ in print("✅ Created new FIDO2 credential: \(response)") session.getAssertionAndAssert(response: response, options: [YKFFIDO2OptionUP: false]) { response in print("✅ Asserted FIDO2 credential: \(response)") @@ -250,9 +250,19 @@ class FIDO2Tests: XCTestCase { runYubiKitTest { connection, completion in connection.fido2TestSession { session in let createExtensions = ["sign" : ["generateKey": ["algorithms": [-65539]]]] - session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmES256, options: [YKFFIDO2OptionRK: false, YKFFIDO2OptionUV: false], extensions: createExtensions) { response in + session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmES256, options: [YKFFIDO2OptionRK: false, YKFFIDO2OptionUV: false], extensions: createExtensions) { response, extensionResponse in print(response.authenticatorData) - print("✅ Created new FIDO2 credential: \(response)") + print("✅ Created new FIDO2 credential: \(response), \(extensionResponse)") + + let credentialId = (response.authenticatorData!.credentialId! as NSData).ykf_websafeBase64EncodedString() + let sign = (extensionResponse!["sign"] as! [String: Any]) + let generatedKey = sign["generatedKey"] as! [String: Any] + let keyHandle = generatedKey["keyHandle"] as! String + let assertExtensions = ["sign" : ["sign" : ["phData" : "Mp80eo3FdxJ2hLtD7HHQO5hhzjyDD57Kdvuqi_weTGE", "keyHandleByCredential" : [credentialId: keyHandle]]]] + session.getAssertionAndAssert(response: response, options: [YKFFIDO2OptionUP: true, YKFFIDO2OptionUV: false], extensions: assertExtensions) { response in + print("✅ Asserted FIDO2 credential: \(response)") + completion() + } } } } @@ -264,7 +274,7 @@ class FIDO2Tests: XCTestCase { session.verifyPin("123456") { error in if let error { XCTFail("verifyPin failed with: \(error)"); return } let createExtensions = ["prf" : []] - session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmES256, options: [YKFFIDO2OptionRK: true], extensions: createExtensions) { response in + session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmES256, options: [YKFFIDO2OptionRK: true], extensions: createExtensions) { response, _ in print(response.authenticatorData) print("✅ Created new FIDO2 credential: \(response)") @@ -286,7 +296,7 @@ class FIDO2Tests: XCTestCase { if let error { XCTFail("verifyPin failed with: \(error)"); return } // let extensions = ["largeBlobKey" : ["support": "required"]] // session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmEdDSA, options: [YKFFIDO2OptionRK: true], extensions: extensions) { response in - session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmEdDSA, options: [YKFFIDO2OptionRK: true]) { response in + session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmEdDSA, options: [YKFFIDO2OptionRK: true]) { response, _ in print("✅ Created new FIDO2 credential: \(response)") session.getAssertionAndAssert(response: response, options: [YKFFIDO2OptionUP: true]) { response in print("✅ Asserted FIDO2 credential: \(response)") @@ -300,7 +310,7 @@ class FIDO2Tests: XCTestCase { } extension YKFFIDO2Session { - func addCredentialAndAssert(algorithm: Int, options: [String: Any]? = nil, extensions: [String: Any]? = nil, completion: @escaping (_ response: YKFFIDO2MakeCredentialResponse) -> Void) { + func addCredentialAndAssert(algorithm: Int, options: [String: Any]? = nil, extensions: [String: Any]? = nil, completion: @escaping (_ response: YKFFIDO2MakeCredentialResponse, _ clientExtensionResults: [AnyHashable: Any]?) -> Void) { addCredential(algorithm: algorithm, options: options, extensions: extensions) { response, clientExtensionResults, error in if let clientExtensionResults { let jsonData = try! JSONSerialization.data(withJSONObject: clientExtensionResults) @@ -308,7 +318,7 @@ extension YKFFIDO2Session { print(jsonString as Any) } guard let response = response else { XCTAssertTrue(false, "🔴 Failed making FIDO2 credential: \(error!)"); return } - completion(response) + completion(response, clientExtensionResults) } }