diff --git a/YubiKit/YubiKit/Connections/Shared/APDU/FIDO2/YKFFIDO2MakeCredentialAPDU.m b/YubiKit/YubiKit/Connections/Shared/APDU/FIDO2/YKFFIDO2MakeCredentialAPDU.m index c5cf2c79..86c652d0 100644 --- a/YubiKit/YubiKit/Connections/Shared/APDU/FIDO2/YKFFIDO2MakeCredentialAPDU.m +++ b/YubiKit/YubiKit/Connections/Shared/APDU/FIDO2/YKFFIDO2MakeCredentialAPDU.m @@ -103,48 +103,12 @@ - (nullable instancetype)initWithClientDataHash:(NSData *)clientDataHash } requestDictionary[YKFCBORInteger(YKFFIDO2MakeCredentialAPDUKeyOptions)] = YKFCBORMap(mutableOptions); } - - // Extensions - NSMutableDictionary *extensionsDict = [NSMutableDictionary new]; - // Sign - if (extensions && extensions[@"sign"] && extensions[@"sign"][@"generateKey"]) { - NSDictionary *generateKeyDict = (NSDictionary *) extensions[@"sign"][@"generateKey"]; - NSMutableDictionary *signExtensionDict = [NSMutableDictionary new]; - // Flags hard coded for now. More information here: - // https://github.com/Yubico/python-fido2/blob/8722a8925509d3320f8cb6d8a22c76e2af08fb20/fido2/ctap2/extensions.py#L493 - int flags = 0b101; - - NSMutableArray *algorithms = [NSMutableArray array]; - [(NSArray *)generateKeyDict[@"algorithms"] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { - NSInteger intValue = [(NSNumber *)obj integerValue]; - [algorithms addObject:YKFCBORInteger(intValue)]; - }]; - signExtensionDict[YKFCBORInteger(3)] = YKFCBORArray(algorithms); - signExtensionDict[YKFCBORInteger(4)] = YKFCBORInteger(flags); - - if (generateKeyDict[@"phData"]) { - NSString * phData = generateKeyDict[@"phData"]; - NSData *phDataBase64Encoded = [[NSData alloc] initWithBase64EncodedString:phData options:0]; - signExtensionDict[YKFCBORInteger(0)] = YKFCBORByteString(phDataBase64Encoded); - } - extensionsDict[YKFCBORTextString(@"sign")] = YKFCBORMap(signExtensionDict); - } - - // Extensions large blob - if (extensions && extensions[@"largeBlobKey"] && [extensions[@"largeBlobKey"][@"support"] isEqual: @"required"]) { - extensionsDict[YKFCBORTextString(@"largeBlobKey")] = YKFCBORBool(true); - } - // Extensions hmac-secret - if (extensions && extensions[@"hmac-secret"]) { - extensionsDict[YKFCBORTextString(@"hmac-secret")] = YKFCBORBool(true); - } - - if (extensionsDict.count > 0) { - requestDictionary[YKFCBORInteger(YKFFIDO2MakeCredentialAPDUKeyExtensions)] = YKFCBORMap(extensionsDict); + // Extensions + if (extensions.count > 0) { + requestDictionary[YKFCBORInteger(YKFFIDO2MakeCredentialAPDUKeyExtensions)] = YKFCBORMap(extensions); } - // Pin Auth if (pinAuth) { requestDictionary[YKFCBORInteger(YKFFIDO2MakeCredentialAPDUKeyPinAuth)] = YKFCBORByteString(pinAuth); diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.h b/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.h index c5b54eea..bd6e2189 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.h +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.h @@ -101,7 +101,7 @@ typedef void (^YKFFIDO2SessionGetInfoCompletionBlock) parameter is nil. */ typedef void (^YKFFIDO2SessionMakeCredentialCompletionBlock) - (YKFFIDO2MakeCredentialResponse* _Nullable response, NSError* _Nullable error); + (YKFFIDO2MakeCredentialResponse* _Nullable response, NSDictionary* _Nullable clientExtensionsOutput, NSError* _Nullable error); /*! @abstract diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.m b/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.m index 7c770b06..ba2cb85f 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.m +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.m @@ -36,6 +36,7 @@ #import "YKFFIDO2ResetAPDU.h" #import "YKFFIDO2GetInfoResponse+Private.h" +#import "YKFFIDO2MakeCredentialResponse.h" #import "YKFFIDO2MakeCredentialResponse+Private.h" #import "YKFFIDO2GetAssertionResponse+Private.h" @@ -335,15 +336,52 @@ - (void)makeCredentialWithClientDataHash:(NSData *)clientDataHash NSData *hmac = [clientDataHash ykf_fido2HMACWithKey:self.pinToken]; pinAuth = [hmac subdataWithRange:NSMakeRange(0, 16)]; if (!pinAuth) { - completion(nil, [YKFFIDO2Error errorWithCode:YKFFIDO2ErrorCodeOTHER]); + completion(nil, nil, [YKFFIDO2Error errorWithCode:YKFFIDO2ErrorCodeOTHER]); } } - YKFAPDU *apdu = [[YKFFIDO2MakeCredentialAPDU alloc] initWithClientDataHash:clientDataHash rp:rp user:user pubKeyCredParams:pubKeyCredParams excludeList:excludeList pinAuth:pinAuth pinProtocol:pinProtocol options:options extensions:extensions]; + + // Extensions, client authentictor input + NSMutableDictionary *authenticatorInputs = [NSMutableDictionary new]; + // Sign + if (extensions && extensions[@"sign"] && extensions[@"sign"][@"generateKey"]) { + NSDictionary *generateKeyDict = (NSDictionary *) extensions[@"sign"][@"generateKey"]; + NSMutableDictionary *signExtensionDict = [NSMutableDictionary new]; + // Flags hard coded for now. More information here: + // https://github.com/Yubico/python-fido2/blob/8722a8925509d3320f8cb6d8a22c76e2af08fb20/fido2/ctap2/extensions.py#L493 + int flags = 0b101; + + NSMutableArray *algorithms = [NSMutableArray array]; + [(NSArray *)generateKeyDict[@"algorithms"] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + NSInteger intValue = [(NSNumber *)obj integerValue]; + [algorithms addObject:YKFCBORInteger(intValue)]; + }]; + signExtensionDict[YKFCBORInteger(3)] = YKFCBORArray(algorithms); + signExtensionDict[YKFCBORInteger(4)] = YKFCBORInteger(flags); + + if (generateKeyDict[@"phData"]) { + NSString * phData = generateKeyDict[@"phData"]; + NSData *phDataBase64Encoded = [[NSData alloc] initWithBase64EncodedString:phData options:0]; + signExtensionDict[YKFCBORInteger(0)] = YKFCBORByteString(phDataBase64Encoded); + } + authenticatorInputs[YKFCBORTextString(@"sign")] = YKFCBORMap(signExtensionDict); + } + + // Extensions large blob + if (extensions && extensions[@"largeBlobKey"] && [extensions[@"largeBlobKey"][@"support"] isEqual: @"required"]) { + authenticatorInputs[YKFCBORTextString(@"largeBlobKey")] = YKFCBORBool(true); + } + + // Extensions hmac-secret + if (extensions && extensions[@"prf"]) { + authenticatorInputs[YKFCBORTextString(@"hmac-secret")] = YKFCBORBool(true); + } + + YKFAPDU *apdu = [[YKFFIDO2MakeCredentialAPDU alloc] initWithClientDataHash:clientDataHash rp:rp user:user pubKeyCredParams:pubKeyCredParams excludeList:excludeList pinAuth:pinAuth pinProtocol:pinProtocol options:options extensions:authenticatorInputs]; if (!apdu) { YKFSessionError *error = [YKFFIDO2Error errorWithCode:YKFFIDO2ErrorCodeOTHER]; - completion(nil, error); + completion(nil, nil, error); return; } @@ -351,17 +389,35 @@ - (void)makeCredentialWithClientDataHash:(NSData *)clientDataHash [self executeFIDO2Command:apdu retryCount:0 completion:^(NSData *data, NSError *error) { ykf_safe_strong_self(); if (error) { - completion(nil, error); + completion(nil, nil, error); return; } NSData *cborData = [strongSelf cborFromKeyResponseData:data]; YKFFIDO2MakeCredentialResponse *makeCredentialResponse = [[YKFFIDO2MakeCredentialResponse alloc] initWithCBORData:cborData]; + // Extensions, authenticator output + NSMutableDictionary *extensionsClientOutput = [NSMutableDictionary new]; + if (authenticatorInputs[YKFCBORTextString(@"hmac-secret")]) { + YKFCBORBool *cborBool = makeCredentialResponse.authenticatorData.extensions.value[YKFCBORTextString(@"hmac-secret")]; + if (cborBool && cborBool.value) { + extensionsClientOutput[@"prf"] = @{@"enabled" : @YES}; + } else { + extensionsClientOutput[@"prf"] = @{@"enabled" : @NO}; + } +// +// NSError *error; +// NSData *jsonData = [NSJSONSerialization dataWithJSONObject:extensionsClientOutput +// options:NSJSONWritingPrettyPrinted +// error:&error]; +// NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; +// NSLog(@"%@", jsonString); + } + if (makeCredentialResponse) { - completion(makeCredentialResponse, nil); + completion(makeCredentialResponse, extensionsClientOutput, nil); } else { - completion(nil, [YKFFIDO2Error errorWithCode:YKFFIDO2ErrorCodeINVALID_CBOR]); + completion(nil, nil, [YKFFIDO2Error errorWithCode:YKFFIDO2ErrorCodeINVALID_CBOR]); } }]; } diff --git a/YubiKitTests/Tests/FIDO2Tests.swift b/YubiKitTests/Tests/FIDO2Tests.swift index 1d3534c6..04d0d85b 100644 --- a/YubiKitTests/Tests/FIDO2Tests.swift +++ b/YubiKitTests/Tests/FIDO2Tests.swift @@ -129,7 +129,7 @@ class FIDO2Tests: XCTestCase { if connection as? YKFNFCConnection != nil { connection.fido2TestSession { session in session.setPin("123456") { _ in - session.addCredential(algorithm: YKFFIDO2PublicKeyAlgorithmES256, options: [YKFFIDO2OptionRK: false]) { response, error in + session.addCredential(algorithm: YKFFIDO2PublicKeyAlgorithmES256, options: [YKFFIDO2OptionRK: false]) { response, _, error in if let error = error { XCTAssertTrue((error as NSError).code == 54, "🔴 Unexpected error: \(error)") } else { @@ -246,12 +246,12 @@ class FIDO2Tests: XCTestCase { } } - func testCreateHmacSecretExtensionCredential() { + func testCreatePRFSecretExtensionCredential() { runYubiKitTest { connection, completion in connection.fido2TestSession { session in session.verifyPin("123456") { error in if let error { XCTFail("verifyPin failed with: \(error)"); return } - let extensions = ["hmac-secret" : true] + let extensions = ["prf" : true] session.addCredentialAndAssert(algorithm: YKFFIDO2PublicKeyAlgorithmEdDSA, options: [YKFFIDO2OptionRK: true], extensions: extensions) { response in print(response.authenticatorData) print("✅ Created new FIDO2 credential: \(response)") @@ -308,7 +308,12 @@ class FIDO2Tests: XCTestCase { extension YKFFIDO2Session { func addCredentialAndAssert(algorithm: Int, options: [String: Any]? = nil, extensions: [String: Any]? = nil, completion: @escaping (_ response: YKFFIDO2MakeCredentialResponse) -> Void) { - addCredential(algorithm: algorithm, options: options, extensions: extensions) { response, error in + addCredential(algorithm: algorithm, options: options, extensions: extensions) { response, clientExtensionResults, error in + if let clientExtensionResults { + let jsonData = try! JSONSerialization.data(withJSONObject: clientExtensionResults) + let jsonString = String(data: jsonData, encoding: .utf8) + print(jsonString as Any) + } guard let response = response else { XCTAssertTrue(false, "🔴 Failed making FIDO2 credential: \(error!)"); return } completion(response) }