From 8fe96e9e52aa6ceddb81eb157fda7aa0f2cd4dee Mon Sep 17 00:00:00 2001 From: Vince Ricosti <80467769+vricosti@users.noreply.github.com> Date: Thu, 2 Jan 2025 20:28:11 +0100 Subject: [PATCH 01/11] Commit all --- ios/TcpSocketClient.m | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/ios/TcpSocketClient.m b/ios/TcpSocketClient.m index 80e22d5..fd94516 100644 --- a/ios/TcpSocketClient.m +++ b/ios/TcpSocketClient.m @@ -13,7 +13,7 @@ @interface TcpSocketClient () { BOOL _checkValidity; BOOL _paused; BOOL _connecting; - NSString *_certPath; + NSString *_caCertPath; NSString *_host; GCDAsyncSocket *_tcpSocket; NSMutableDictionary *_pendingSends; @@ -134,7 +134,7 @@ - (void)startTLS:(NSDictionary *)tlsOptions { forKey:GCDAsyncSocketManuallyEvaluateTrust]; } else if (certResourcePath != nil) { // Self-signed certificate - _certPath = certResourcePath; + _caCertPath = certResourcePath; [settings setObject:[NSNumber numberWithBool:YES] forKey:GCDAsyncSocketManuallyEvaluateTrust]; } else { @@ -406,39 +406,39 @@ - (void)socket:(GCDAsyncSocket *)sock length:(NSUInteger)serverDataSize]; // Local certificate - NSURL *certUrl = [[NSURL alloc] initWithString:_certPath]; - NSString *pem = [[NSString alloc] initWithContentsOfURL:certUrl + NSURL *caCertUrl = [[NSURL alloc] initWithString:_caCertPath]; + NSString *pemCaCert = [[NSString alloc] initWithContentsOfURL:caCertUrl encoding:NSUTF8StringEncoding error:NULL]; // Strip PEM header and footers. We don't support multi-certificate PEM. - NSMutableString *pemMutable = - [pem stringByTrimmingCharactersInSet: + NSMutableString *pemCaCertMutable = + [pemCaCert stringByTrimmingCharactersInSet: NSCharacterSet.whitespaceAndNewlineCharacterSet] .mutableCopy; // Strip PEM header and footer - [pemMutable + [pemCaCertMutable replaceOccurrencesOfString:@"-----BEGIN CERTIFICATE-----" withString:@"" options:(NSStringCompareOptions)(NSAnchoredSearch | NSLiteralSearch) - range:NSMakeRange(0, pemMutable.length)]; + range:NSMakeRange(0, pemCaCertMutable.length)]; - [pemMutable + [pemCaCertMutable replaceOccurrencesOfString:@"-----END CERTIFICATE-----" withString:@"" options:(NSStringCompareOptions)(NSAnchoredSearch | NSBackwardsSearch | NSLiteralSearch) - range:NSMakeRange(0, pemMutable.length)]; + range:NSMakeRange(0, pemCaCertMutable.length)]; - NSData *pemData = [[NSData alloc] - initWithBase64EncodedString:pemMutable + NSData *pemCaCertData = [[NSData alloc] + initWithBase64EncodedString:pemCaCertMutable options: NSDataBase64DecodingIgnoreUnknownCharacters]; SecCertificateRef localCertificate = - SecCertificateCreateWithData(NULL, (CFDataRef)pemData); + SecCertificateCreateWithData(NULL, (CFDataRef)pemCaCertData); if (!localCertificate) { [NSException raise:@"Configuration invalid" format:@"Failed to parse PEM certificate"]; From 20031636a024aab5033acfad40eac30a1e2bb0a5 Mon Sep 17 00:00:00 2001 From: Vince Ricosti <80467769+vricosti@users.noreply.github.com> Date: Thu, 2 Jan 2025 20:28:11 +0100 Subject: [PATCH 02/11] update ios code --- ios/TcpSocketClient.h | 12 +++++++++ ios/TcpSocketClient.m | 61 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/ios/TcpSocketClient.h b/ios/TcpSocketClient.h index abbdd82..8405676 100644 --- a/ios/TcpSocketClient.h +++ b/ios/TcpSocketClient.h @@ -19,6 +19,18 @@ typedef enum RCTTCPError RCTTCPError; @class TcpSocketClient; +// Add ResolvableOption interface here +@interface ResolvableOption : NSObject + +@property (nonatomic, strong, readonly) NSString *value; +@property (nonatomic, readonly) BOOL needsResolution; + +- (instancetype)initWithValue:(NSString *)value needsResolution:(BOOL)needsResolution; ++ (instancetype)optionWithValue:(NSString *)value needsResolution:(BOOL)needsResolution; +- (NSString *)resolve; + +@end + @protocol SocketClientDelegate - (void)addClient:(TcpSocketClient *)client; diff --git a/ios/TcpSocketClient.m b/ios/TcpSocketClient.m index fd94516..6633674 100644 --- a/ios/TcpSocketClient.m +++ b/ios/TcpSocketClient.m @@ -7,13 +7,45 @@ NSString *const RCTTCPErrorDomain = @"RCTTCPErrorDomain"; +@implementation ResolvableOption + +- (instancetype)initWithValue:(NSString *)value needsResolution:(BOOL)needsResolution { + if (self = [super init]) { + _value = value; + _needsResolution = needsResolution; + } + return self; +} + ++ (instancetype)optionWithValue:(NSString *)value needsResolution:(BOOL)needsResolution { + return [[self alloc] initWithValue:value needsResolution:needsResolution]; +} + +- (NSString *)resolve { + if (!self.needsResolution) { + return self.value; + } + + NSURL *url = [[NSURL alloc] initWithString:self.value]; + NSError *error = nil; + NSString *contents = [[NSString alloc] initWithContentsOfURL:url + encoding:NSUTF8StringEncoding + error:&error]; + if (error) { + return nil; + } + return contents; +} + +@end + @interface TcpSocketClient () { @private BOOL _tls; BOOL _checkValidity; BOOL _paused; BOOL _connecting; - NSString *_caCertPath; + ResolvableOption *_resolvableCaCert; NSString *_host; GCDAsyncSocket *_tcpSocket; NSMutableDictionary *_pendingSends; @@ -120,10 +152,22 @@ - (BOOL)connect:(NSString *)host return result; } +- (ResolvableOption *)getResolvableOption:(NSDictionary *)tlsOptions forKey:(NSString *)key { + if (![tlsOptions objectForKey:key]) { + return nil; + } + + NSString *value = tlsOptions[key]; + NSArray *resolvedKeys = tlsOptions[@"resolvedKeys"]; + BOOL needsResolution = resolvedKeys != nil && [resolvedKeys containsObject:key]; + + return [ResolvableOption optionWithValue:value needsResolution:needsResolution]; +} + - (void)startTLS:(NSDictionary *)tlsOptions { if (_tls) return; NSMutableDictionary *settings = [NSMutableDictionary dictionary]; - NSString *certResourcePath = tlsOptions[@"ca"]; + _resolvableCaCert = [self getResolvableOption:tlsOptions forKey:@"ca"]; BOOL checkValidity = (tlsOptions[@"rejectUnauthorized"] ? [tlsOptions[@"rejectUnauthorized"] boolValue] : true); @@ -132,9 +176,8 @@ - (void)startTLS:(NSDictionary *)tlsOptions { _checkValidity = false; [settings setObject:[NSNumber numberWithBool:YES] forKey:GCDAsyncSocketManuallyEvaluateTrust]; - } else if (certResourcePath != nil) { + } else if (_resolvableCaCert != nil) { // Self-signed certificate - _caCertPath = certResourcePath; [settings setObject:[NSNumber numberWithBool:YES] forKey:GCDAsyncSocketManuallyEvaluateTrust]; } else { @@ -406,10 +449,12 @@ - (void)socket:(GCDAsyncSocket *)sock length:(NSUInteger)serverDataSize]; // Local certificate - NSURL *caCertUrl = [[NSURL alloc] initWithString:_caCertPath]; - NSString *pemCaCert = [[NSString alloc] initWithContentsOfURL:caCertUrl - encoding:NSUTF8StringEncoding - error:NULL]; + NSString *pemCaCert = [_resolvableCaCert resolve]; + if (!pemCaCert) { + RCTLogWarn(@"Failed to resolve CA certificate"); + completionHandler(NO); + return; + } // Strip PEM header and footers. We don't support multi-certificate PEM. NSMutableString *pemCaCertMutable = From db4ab8a30e3d6bfb0e0b529ce9a5239c83effaf9 Mon Sep 17 00:00:00 2001 From: Vince Ricosti <80467769+vricosti@users.noreply.github.com> Date: Thu, 2 Jan 2025 20:28:11 +0100 Subject: [PATCH 03/11] add trace for debugging --- ios/TcpSocketClient.h | 12 ++ ios/TcpSocketClient.m | 379 +++++++++++++++++++++++++++++++++++++++--- ios/TcpSockets.m | 29 ++++ 3 files changed, 401 insertions(+), 19 deletions(-) diff --git a/ios/TcpSocketClient.h b/ios/TcpSocketClient.h index 8405676..bb82765 100644 --- a/ios/TcpSocketClient.h +++ b/ios/TcpSocketClient.h @@ -127,4 +127,16 @@ typedef enum RCTTCPError RCTTCPError; - (void)resume; +/** + * Get peer certificate information + * @return NSDictionary with certificate information or nil if not available + */ +- (NSDictionary *)getPeerCertificate; + +/** + * Get local certificate information + * @return NSDictionary with certificate information or nil if not available + */ +- (NSDictionary *)getCertificate; + @end diff --git a/ios/TcpSocketClient.m b/ios/TcpSocketClient.m index 6633674..6051e51 100644 --- a/ios/TcpSocketClient.m +++ b/ios/TcpSocketClient.m @@ -46,6 +46,8 @@ @interface TcpSocketClient () { BOOL _paused; BOOL _connecting; ResolvableOption *_resolvableCaCert; + ResolvableOption *_resolvableKey; + ResolvableOption *_resolvableCert; NSString *_host; GCDAsyncSocket *_tcpSocket; NSMutableDictionary *_pendingSends; @@ -168,26 +170,129 @@ - (void)startTLS:(NSDictionary *)tlsOptions { if (_tls) return; NSMutableDictionary *settings = [NSMutableDictionary dictionary]; _resolvableCaCert = [self getResolvableOption:tlsOptions forKey:@"ca"]; + _resolvableKey = [self getResolvableOption:tlsOptions forKey:@"key"]; + _resolvableCert = [self getResolvableOption:tlsOptions forKey:@"cert"]; + + RCTLogWarn(@"startTLS: Resolved options - CA: %@, Key exists: %@, Cert exists: %@", + _resolvableCaCert ? @"YES" : @"NO", + _resolvableKey ? @"YES" : @"NO", + _resolvableCert ? @"YES" : @"NO"); + BOOL checkValidity = (tlsOptions[@"rejectUnauthorized"] ? [tlsOptions[@"rejectUnauthorized"] boolValue] : true); if (!checkValidity) { // Do not validate + RCTLogWarn(@"startTLS: Validation disabled"); _checkValidity = false; [settings setObject:[NSNumber numberWithBool:YES] forKey:GCDAsyncSocketManuallyEvaluateTrust]; } else if (_resolvableCaCert != nil) { // Self-signed certificate + RCTLogWarn(@"startTLS: Using self-signed certificate validation"); [settings setObject:[NSNumber numberWithBool:YES] forKey:GCDAsyncSocketManuallyEvaluateTrust]; } else { // Default certificates + RCTLogWarn(@"startTLS: Using default certificate validation with host: %@", _host); [settings setObject:_host forKey:(NSString *)kCFStreamSSLPeerName]; } + + // Handle client certificate authentication + if (_resolvableCert != nil && _resolvableKey != nil) { + RCTLogWarn(@"startTLS: Attempting client certificate authentication"); + NSString *pemCert = [_resolvableCert resolve]; + NSString *pemKey = [_resolvableKey resolve]; + + RCTLogWarn(@"startTLS: Resolved PEM cert exists: %@, PEM key exists: %@", + pemCert ? @"YES" : @"NO", + pemKey ? @"YES" : @"NO"); + + if (pemCert && pemKey) { + SecIdentityRef identity = [self createIdentityWithCert:pemCert privateKey:pemKey]; + RCTLogWarn(@"startTLS: Identity creation %@", identity ? @"successful" : @"failed"); + if (identity) { + NSArray *certificates = @[(__bridge id)identity]; + [settings setObject:certificates forKey:(NSString *)kCFStreamSSLCertificates]; + CFRelease(identity); + RCTLogWarn(@"startTLS: Client certificates configured successfully"); + } else { + RCTLogWarn(@"startTLS: Failed to create identity from cert and key"); + } + } + } + + RCTLogWarn(@"startTLS: Final settings: %@", settings); _tls = true; [_tcpSocket startTLS:settings]; } +- (SecIdentityRef)createIdentityWithCert:(NSString *)pemCert privateKey:(NSString *)pemKey { + // Strip PEM headers and convert to data + NSString *cleanedCert = [self stripPEMHeader:pemCert prefix:@"CERTIFICATE"]; + NSString *cleanedKey = [self stripPEMHeader:pemKey prefix:@"PRIVATE KEY"]; + + NSData *certData = [[NSData alloc] initWithBase64EncodedString:cleanedCert options:NSDataBase64DecodingIgnoreUnknownCharacters]; + NSData *keyData = [[NSData alloc] initWithBase64EncodedString:cleanedKey options:NSDataBase64DecodingIgnoreUnknownCharacters]; + + if (!certData || !keyData) { + return NULL; + } + + // Create certificate + SecCertificateRef cert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData); + if (!cert) { + return NULL; + } + + // Import private key + NSDictionary *options = @{ + (__bridge id)kSecImportExportPassphrase: @"", + (__bridge id)kSecImportExportKeyUsage: @YES, + }; + + CFArrayRef items = NULL; + OSStatus status = SecPKCS12Import((__bridge CFDataRef)keyData, + (__bridge CFDictionaryRef)options, + &items); + + if (status != errSecSuccess) { + if (cert) CFRelease(cert); + return NULL; + } + + // Create identity + SecIdentityRef identity = NULL; + status = SecIdentityCreateWithCertificate(NULL, cert, &identity); + + if (cert) CFRelease(cert); + if (items) CFRelease(items); + + return identity; +} + +- (NSString *)stripPEMHeader:(NSString *)pemData prefix:(NSString *)prefix { + NSMutableString *cleaned = [pemData mutableCopy]; + + // Remove header + NSString *header = [NSString stringWithFormat:@"-----BEGIN %@-----", prefix]; + [cleaned replaceOccurrencesOfString:header + withString:@"" + options:NSLiteralSearch + range:NSMakeRange(0, cleaned.length)]; + + // Remove footer + NSString *footer = [NSString stringWithFormat:@"-----END %@-----", prefix]; + [cleaned replaceOccurrencesOfString:footer + withString:@"" + options:NSLiteralSearch + range:NSMakeRange(0, cleaned.length)]; + + // Remove whitespace and newlines + return [[cleaned componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] + componentsJoinedByString:@""]; +} + - (NSDictionary *)getAddress { if (_tcpSocket) { if (_tcpSocket.isConnected) { @@ -463,25 +568,11 @@ - (void)socket:(GCDAsyncSocket *)sock .mutableCopy; // Strip PEM header and footer - [pemCaCertMutable - replaceOccurrencesOfString:@"-----BEGIN CERTIFICATE-----" - withString:@"" - options:(NSStringCompareOptions)(NSAnchoredSearch | - NSLiteralSearch) - range:NSMakeRange(0, pemCaCertMutable.length)]; - - [pemCaCertMutable - replaceOccurrencesOfString:@"-----END CERTIFICATE-----" - withString:@"" - options:(NSStringCompareOptions)(NSAnchoredSearch | - NSBackwardsSearch | - NSLiteralSearch) - range:NSMakeRange(0, pemCaCertMutable.length)]; - - NSData *pemCaCertData = [[NSData alloc] - initWithBase64EncodedString:pemCaCertMutable - options: - NSDataBase64DecodingIgnoreUnknownCharacters]; + // Use the stripPEMHeader helper method + NSString *cleanedCaCert = [self stripPEMHeader:pemCaCert prefix:@"CERTIFICATE"]; + NSData *pemCaCertData = [[NSData alloc] initWithBase64EncodedString:cleanedCaCert + options:NSDataBase64DecodingIgnoreUnknownCharacters]; + SecCertificateRef localCertificate = SecCertificateCreateWithData(NULL, (CFDataRef)pemCaCertData); if (!localCertificate) { @@ -541,6 +632,255 @@ - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err { withError:(!err || err.code == GCDAsyncSocketClosedError ? nil : err)]; } +- (NSDictionary *)getPeerCertificate { + if (!_tcpSocket || !_tls) { + return nil; + } + + SecTrustRef trust = (__bridge SecTrustRef)(_tcpSocket.sslPeerTrust); + if (!trust) { + return nil; + } + + SecCertificateRef certificate = SecTrustGetCertificateAtIndex(trust, 0); + if (!certificate) { + return nil; + } + + return [self certificateToDict:certificate detailed:YES]; +} + +- (NSDictionary *)getCertificate { + if (!_tcpSocket || !_tls || !_resolvableCert) { + return nil; + } + + NSString *pemCert = [_resolvableCert resolve]; + if (!pemCert) { + return nil; + } + + NSString *cleanedCert = [self stripPEMHeader:pemCert prefix:@"CERTIFICATE"]; + NSData *certData = [[NSData alloc] initWithBase64EncodedString:cleanedCert + options:NSDataBase64DecodingIgnoreUnknownCharacters]; + + SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData); + if (!certificate) { + return nil; + } + + NSDictionary *result = [self certificateToDict:certificate detailed:YES]; + CFRelease(certificate); + + return result; +} + +- (NSDictionary *)certificateToDict:(SecCertificateRef)certificate detailed:(BOOL)detailed { + NSMutableDictionary *certInfo = [NSMutableDictionary dictionary]; + + // Get public key info + SecKeyRef publicKey = NULL; + OSStatus status = SecCertificateCopyPublicKey(certificate, &publicKey); + if (status == errSecSuccess && publicKey) { + CFDictionaryRef attributes = SecKeyCopyAttributes(publicKey); + if (attributes) { + // Get key size (bits) + NSNumber *keySize = CFDictionaryGetValue(attributes, kSecAttrKeySizeInBits); + certInfo[@"bits"] = keySize; + + // Get modulus and exponent for RSA keys + CFDataRef keyData = SecKeyCopyExternalRepresentation(publicKey, NULL); + if (keyData) { + NSData *keyDataNS = (__bridge NSData *)keyData; + + // For RSA, the external representation is a DER-encoded RSAPublicKey + if ([self isRSAKey:publicKey]) { + NSArray *components = [self parseRSAPublicKey:keyDataNS]; + if (components.count == 2) { + certInfo[@"modulus"] = components[0]; + certInfo[@"exponent"] = [NSString stringWithFormat:@"0x%@", components[1]]; + } + } + + // Add base64 encoded public key + certInfo[@"pubkey"] = [keyDataNS base64EncodedStringWithOptions:0]; + CFRelease(keyData); + } + CFRelease(attributes); + } + CFRelease(publicKey); + } + + // Get subject DN + CFDictionaryRef subjectName = SecCertificateCopyNormalizedSubjectContent(certificate, &status); + if (status == errSecSuccess && subjectName) { + certInfo[@"subject"] = [self parseDN:(__bridge NSDictionary *)subjectName]; + CFRelease(subjectName); + } + + // Get issuer DN + CFDictionaryRef issuerName = SecCertificateCopyNormalizedIssuerContent(certificate, &status); + if (status == errSecSuccess && issuerName) { + certInfo[@"issuer"] = [self parseDN:(__bridge NSDictionary *)issuerName]; + CFRelease(issuerName); + } + + // Get validity dates + CFDictionaryRef values = NULL; + status = SecCertificateCopyValues(certificate, NULL, &values); + if (status == errSecSuccess && values) { + CFDictionaryRef validityPeriod = CFDictionaryGetValue(values, kSecOIDValidityPeriod); + if (validityPeriod) { + CFArrayRef validityArray = CFDictionaryGetValue(validityPeriod, kSecPropertyKeyValue); + if (validityArray && CFArrayGetCount(validityArray) == 2) { + CFDictionaryRef notBeforeDict = CFArrayGetValueAtIndex(validityArray, 0); + CFDictionaryRef notAfterDict = CFArrayGetValueAtIndex(validityArray, 1); + + CFStringRef notBefore = CFDictionaryGetValue(notBeforeDict, kSecPropertyKeyValue); + CFStringRef notAfter = CFDictionaryGetValue(notAfterDict, kSecPropertyKeyValue); + + if (notBefore) { + NSDate *fromDate = (__bridge NSDate *)notBefore; + certInfo[@"valid_from"] = [self formatDate:fromDate]; + } + if (notAfter) { + NSDate *toDate = (__bridge NSDate *)notAfter; + certInfo[@"valid_to"] = [self formatDate:toDate]; + } + } + } + + // Check if it's a CA + CFDictionaryRef basicConstraints = CFDictionaryGetValue(values, kSecOIDBasicConstraints); + if (basicConstraints) { + CFTypeRef value = CFDictionaryGetValue(basicConstraints, kSecPropertyKeyValue); + if (value) { + certInfo[@"ca"] = @(CFBooleanGetValue(value)); + } + } + + CFRelease(values); + } + + // Get serial number + NSData *serialData = (__bridge_transfer NSData *)SecCertificateCopySerialNumber(certificate, NULL); + if (serialData) { + NSMutableString *serialHex = [NSMutableString string]; + const unsigned char *bytes = serialData.bytes; + for (NSInteger i = 0; i < serialData.length; i++) { + [serialHex appendFormat:@"%02X", bytes[i]]; + } + certInfo[@"serialNumber"] = serialHex; + } + + // Get fingerprints + NSData *certData = (__bridge_transfer NSData *)SecCertificateCopyData(certificate); + if (certData) { + certInfo[@"fingerprint"] = [self calculateFingerprint:certData algorithm:CC_SHA1 length:CC_SHA1_DIGEST_LENGTH]; + certInfo[@"fingerprint256"] = [self calculateFingerprint:certData algorithm:CC_SHA256 length:CC_SHA256_DIGEST_LENGTH]; + certInfo[@"fingerprint512"] = [self calculateFingerprint:certData algorithm:CC_SHA512 length:CC_SHA512_DIGEST_LENGTH]; + } + + return certInfo; +} + +- (BOOL)isRSAKey:(SecKeyRef)key { + CFDictionaryRef attributes = SecKeyCopyAttributes(key); + if (!attributes) return NO; + + CFStringRef keyType = CFDictionaryGetValue(attributes, kSecAttrKeyType); + BOOL isRSA = keyType && CFEqual(keyType, kSecAttrKeyTypeRSA); + CFRelease(attributes); + + return isRSA; +} + +- (NSArray *)parseRSAPublicKey:(NSData *)keyData { + // Parse DER-encoded RSAPublicKey structure + const uint8_t *bytes = keyData.bytes; + NSInteger length = keyData.length; + + // Skip header and length bytes + if (length < 2 || bytes[0] != 0x30) return nil; + + NSInteger idx = 2; + if (bytes[1] & 0x80) { + idx += (bytes[1] & 0x7F); + } + + // Read modulus + if (idx >= length || bytes[idx] != 0x02) return nil; + idx++; + + NSInteger modulusLength = bytes[idx++]; + if (modulusLength & 0x80) { + int lenBytes = modulusLength & 0x7F; + modulusLength = 0; + for (int i = 0; i < lenBytes; i++) { + modulusLength = (modulusLength << 8) | bytes[idx++]; + } + } + + NSMutableString *modulus = [NSMutableString string]; + for (NSInteger i = 0; i < modulusLength; i++) { + [modulus appendFormat:@"%02X", bytes[idx + i]]; + } + idx += modulusLength; + + // Read exponent + if (idx >= length || bytes[idx] != 0x02) return nil; + idx++; + + NSInteger exponentLength = bytes[idx++]; + NSMutableString *exponent = [NSMutableString string]; + for (NSInteger i = 0; i < exponentLength; i++) { + [exponent appendFormat:@"%02X", bytes[idx + i]]; + } + + return @[modulus, exponent]; +} + +- (NSString *)calculateFingerprint:(NSData *)data algorithm:(unsigned char * (^)(const void *, CC_LONG, unsigned char *))hashFunction length:(CC_LONG)hashLength { + unsigned char digest[hashLength]; + hashFunction(data.bytes, (CC_LONG)data.length, digest); + + NSMutableString *fingerprint = [NSMutableString stringWithCapacity:hashLength * 3]; + for (int i = 0; i < hashLength; i++) { + [fingerprint appendFormat:@"%02X:", digest[i]]; + } + return [fingerprint substringToIndex:fingerprint.length - 1]; // Remove last colon +} + +- (NSDictionary *)parseDN:(NSDictionary *)dnDict { + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + + // Map common DN fields + NSDictionary *mapping = @{ + (id)kSecOIDCommonName: @"CN", + (id)kSecOIDDNQualifier: @"dnQualifier" + }; + + for (id key in dnDict) { + NSString *mappedKey = mapping[key]; + if (mappedKey) { + NSString *value = dnDict[key]; + if ([value isKindOfClass:[NSString class]]) { + result[mappedKey] = value; + } + } + } + + return result; +} + +- (NSString *)formatDate:(NSDate *)date { + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; + formatter.dateFormat = @"MMM dd HH:mm:ss yyyy 'GMT'"; + formatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"]; + return [formatter stringFromDate:date]; +} + - (NSError *)badInvocationError:(NSString *)errMsg { NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg @@ -555,4 +895,5 @@ - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); } + @end diff --git a/ios/TcpSockets.m b/ios/TcpSockets.m index 3a9783a..a689ada 100644 --- a/ios/TcpSockets.m +++ b/ios/TcpSockets.m @@ -175,6 +175,35 @@ - (TcpSocketClient *)createSocket:(nonnull NSNumber *)cId { [client resume]; } +// Method with Promise (modern style) +RCT_EXPORT_METHOD(getPeerCertificate:(nonnull NSNumber *)cId + detailed:(BOOL)detailed + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + TcpSocketClient *client = [self findClient:cId]; + if (!client) { + reject(@"NOT_FOUND", @"Client not found", nil); + return; + } + + NSDictionary *cert = [client getPeerCertificate:detailed]; + resolve(cert ?: [NSNull null]); +} + +RCT_EXPORT_METHOD(getCertificate:(nonnull NSNumber *)cId + detailed:(BOOL)detailed + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + TcpSocketClient *client = [self findClient:cId]; + if (!client) { + reject(@"NOT_FOUND", @"Client not found", nil); + return; + } + + NSDictionary *cert = [client getCertificate:detailed]; + resolve(cert ?: [NSNull null]); +} + - (void)onWrittenData:(TcpSocketClient *)client msgId:(NSNumber *)msgId { [self sendEventWithName:@"written" body:@{ From 9f3645040212cba9821b82f2f9e406d998a91793 Mon Sep 17 00:00:00 2001 From: Vince Ricosti <80467769+vricosti@users.noreply.github.com> Date: Wed, 22 Jan 2025 12:53:48 +0100 Subject: [PATCH 04/11] wip --- ios/TcpSocketClient.m | 503 ++++++++++++++++++++++++++++-------------- 1 file changed, 332 insertions(+), 171 deletions(-) diff --git a/ios/TcpSocketClient.m b/ios/TcpSocketClient.m index 6051e51..b5d48c4 100644 --- a/ios/TcpSocketClient.m +++ b/ios/TcpSocketClient.m @@ -5,6 +5,14 @@ #import +#import +#import +#import +#import +#import +#import +#import + NSString *const RCTTCPErrorDomain = @"RCTTCPErrorDomain"; @implementation ResolvableOption @@ -55,6 +63,7 @@ @interface TcpSocketClient () { NSLock *_lock; NSNumber *_serverId; long _sendTag; + SecTrustRef _peerTrust; } - (id)initWithClientId:(NSNumber *)clientID @@ -172,8 +181,12 @@ - (void)startTLS:(NSDictionary *)tlsOptions { _resolvableCaCert = [self getResolvableOption:tlsOptions forKey:@"ca"]; _resolvableKey = [self getResolvableOption:tlsOptions forKey:@"key"]; _resolvableCert = [self getResolvableOption:tlsOptions forKey:@"cert"]; - - RCTLogWarn(@"startTLS: Resolved options - CA: %@, Key exists: %@, Cert exists: %@", + NSString *keyAlias = tlsOptions[@"keyAlias"] ?: @"key"; + [settings setObject:keyAlias forKey:@"keyAlias"]; + NSString *certAlias = tlsOptions[@"certAlias"] ?: @"cert"; + [settings setObject:certAlias forKey:@"certAlias"]; + + RCTLogWarn(@"startTLS: Resolved options - CA: %@, Key exists: %@, Cert exists: %@", _resolvableCaCert ? @"YES" : @"NO", _resolvableKey ? @"YES" : @"NO", _resolvableCert ? @"YES" : @"NO"); @@ -209,7 +222,7 @@ - (void)startTLS:(NSDictionary *)tlsOptions { pemKey ? @"YES" : @"NO"); if (pemCert && pemKey) { - SecIdentityRef identity = [self createIdentityWithCert:pemCert privateKey:pemKey]; + SecIdentityRef identity = [self createIdentityWithCert:pemCert privateKey:pemKey settings:settings]; RCTLogWarn(@"startTLS: Identity creation %@", identity ? @"successful" : @"failed"); if (identity) { NSArray *certificates = @[(__bridge id)identity]; @@ -227,72 +240,6 @@ - (void)startTLS:(NSDictionary *)tlsOptions { [_tcpSocket startTLS:settings]; } -- (SecIdentityRef)createIdentityWithCert:(NSString *)pemCert privateKey:(NSString *)pemKey { - // Strip PEM headers and convert to data - NSString *cleanedCert = [self stripPEMHeader:pemCert prefix:@"CERTIFICATE"]; - NSString *cleanedKey = [self stripPEMHeader:pemKey prefix:@"PRIVATE KEY"]; - - NSData *certData = [[NSData alloc] initWithBase64EncodedString:cleanedCert options:NSDataBase64DecodingIgnoreUnknownCharacters]; - NSData *keyData = [[NSData alloc] initWithBase64EncodedString:cleanedKey options:NSDataBase64DecodingIgnoreUnknownCharacters]; - - if (!certData || !keyData) { - return NULL; - } - - // Create certificate - SecCertificateRef cert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData); - if (!cert) { - return NULL; - } - - // Import private key - NSDictionary *options = @{ - (__bridge id)kSecImportExportPassphrase: @"", - (__bridge id)kSecImportExportKeyUsage: @YES, - }; - - CFArrayRef items = NULL; - OSStatus status = SecPKCS12Import((__bridge CFDataRef)keyData, - (__bridge CFDictionaryRef)options, - &items); - - if (status != errSecSuccess) { - if (cert) CFRelease(cert); - return NULL; - } - - // Create identity - SecIdentityRef identity = NULL; - status = SecIdentityCreateWithCertificate(NULL, cert, &identity); - - if (cert) CFRelease(cert); - if (items) CFRelease(items); - - return identity; -} - -- (NSString *)stripPEMHeader:(NSString *)pemData prefix:(NSString *)prefix { - NSMutableString *cleaned = [pemData mutableCopy]; - - // Remove header - NSString *header = [NSString stringWithFormat:@"-----BEGIN %@-----", prefix]; - [cleaned replaceOccurrencesOfString:header - withString:@"" - options:NSLiteralSearch - range:NSMakeRange(0, cleaned.length)]; - - // Remove footer - NSString *footer = [NSString stringWithFormat:@"-----END %@-----", prefix]; - [cleaned replaceOccurrencesOfString:footer - withString:@"" - options:NSLiteralSearch - range:NSMakeRange(0, cleaned.length)]; - - // Remove whitespace and newlines - return [[cleaned componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] - componentsJoinedByString:@""]; -} - - (NSDictionary *)getAddress { if (_tcpSocket) { if (_tcpSocket.isConnected) { @@ -538,6 +485,13 @@ - (void)socketDidSecure:(GCDAsyncSocket *)sock { - (void)socket:(GCDAsyncSocket *)sock didReceiveTrust:(SecTrustRef)trust completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler { + + // Store the trust reference + if (_peerTrust) { + CFRelease(_peerTrust); + } + _peerTrust = (SecTrustRef)CFRetain(trust); + // Check if we should check the validity if (!_checkValidity) { completionHandler(YES); @@ -545,8 +499,7 @@ - (void)socket:(GCDAsyncSocket *)sock } // Server certificate - SecCertificateRef serverCertificate = - SecTrustGetCertificateAtIndex(trust, 0); + SecCertificateRef serverCertificate = SecTrustGetCertificateAtIndex(trust, 0); CFDataRef serverCertificateData = SecCertificateCopyData(serverCertificate); const UInt8 *const serverData = CFDataGetBytePtr(serverCertificateData); const CFIndex serverDataSize = CFDataGetLength(serverCertificateData); @@ -561,14 +514,7 @@ - (void)socket:(GCDAsyncSocket *)sock return; } - // Strip PEM header and footers. We don't support multi-certificate PEM. - NSMutableString *pemCaCertMutable = - [pemCaCert stringByTrimmingCharactersInSet: - NSCharacterSet.whitespaceAndNewlineCharacterSet] - .mutableCopy; - // Strip PEM header and footer - // Use the stripPEMHeader helper method NSString *cleanedCaCert = [self stripPEMHeader:pemCaCert prefix:@"CERTIFICATE"]; NSData *pemCaCertData = [[NSData alloc] initWithBase64EncodedString:cleanedCaCert options:NSDataBase64DecodingIgnoreUnknownCharacters]; @@ -632,17 +578,225 @@ - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err { withError:(!err || err.code == GCDAsyncSocketClosedError ? nil : err)]; } -- (NSDictionary *)getPeerCertificate { - if (!_tcpSocket || !_tls) { +// https://stackoverflow.com/questions/45997841/how-to-get-a-secidentityref-from-a-seccertificateref-and-a-seckeyref +// We use a private API call here; it's good enough for WebKit. +SecIdentityRef SecIdentityCreate(CFAllocatorRef allocator, SecCertificateRef certificate, SecKeyRef privateKey); + +- (NSData *)extractRSAKeyFromPKCS8:(NSData *)pkcs8Data { + // PKCS#8 format has a 26-byte header before the RSA private key + // For a proper solution, you should use ASN.1 parsing + // This is a temporary solution that works with standard RSA keys + if (pkcs8Data.length <= 26) { return nil; } + return [pkcs8Data subdataWithRange:NSMakeRange(26, pkcs8Data.length - 26)]; +} + +- (SecIdentityRef)createIdentityWithCert:(NSString *)pemCert + privateKey:(NSString *)pemKey + settings:(NSDictionary *)settings { + RCTLogWarn(@"createIdentity: Starting identity creation"); + + // Strip PEM headers and convert to data + NSString *cleanedCert = [self stripPEMHeader:pemCert prefix:@"CERTIFICATE"]; + NSString *cleanedPrivateKey = [self stripPEMHeader:pemKey prefix:@"PRIVATE KEY"]; + NSString *certAlias = settings[@"certAlias"]; + NSString *keyAlias= settings[@"keyAlias"]; + + NSData *certData = [[NSData alloc] initWithBase64EncodedString:cleanedCert + options:NSDataBase64DecodingIgnoreUnknownCharacters]; + NSData *pkcs8KeyData = [[NSData alloc] initWithBase64EncodedString:cleanedPrivateKey + options:NSDataBase64DecodingIgnoreUnknownCharacters]; + + if (!certData || !pkcs8KeyData) { + RCTLogWarn(@"createIdentity: Failed to create data from base64"); + return NULL; + } + + // Creates a certificate object from its DER representation + SecCertificateRef cert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData); + if (!cert) { + RCTLogWarn(@"createIdentity: Failed to create certificate from data"); + return NULL; + } + + // Extract RSA key from PKCS#8 - TODO use a ASN1 decoder to detect the key format ... + // For my own use I know it's a pem but I prefer not to trust a file extension and + // it's better to check from asn1 data + NSData *rsaKeyData = [self extractRSAKeyFromPKCS8:pkcs8KeyData]; + if (!rsaKeyData) { + RCTLogWarn(@"Failed to extract RSA key from PKCS#8"); + CFRelease(cert); + return NULL; + } + + NSDictionary *privateKeyAttributes = @{ + //(__bridge id)kSecClass: (__bridge id)kSecClassKey, + (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeRSA, + (__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPrivate, + //(__bridge id)kSecAttrLabel: kKeychainPrivateKeyLabel, + //(__bridge id)kSecAttrApplicationTag: [kKeychainApplicationTag dataUsingEncoding:NSUTF8StringEncoding], + }; + + CFErrorRef error = NULL; + SecKeyRef privateKey = SecKeyCreateWithData((__bridge CFDataRef)rsaKeyData, + (__bridge CFDictionaryRef)privateKeyAttributes, + &error); + if (!privateKey) { + RCTLogWarn(@"createIdentity: Failed to create private key: %@", error); + CFRelease(cert); + if (error) CFRelease(error); + return NULL; + } + + NSDictionary *deleteKeyQuery = @{ + (__bridge id)kSecClass: (__bridge id)kSecClassKey, + (__bridge id)kSecAttrLabel: keyAlias + }; + SecItemDelete((__bridge CFDictionaryRef)deleteKeyQuery); - SecTrustRef trust = (__bridge SecTrustRef)(_tcpSocket.sslPeerTrust); - if (!trust) { + // Import certificate in keychain + NSDictionary *deleteCertQuery = @{ + (__bridge id)kSecClass: (__bridge id)kSecClassCertificate, + (__bridge id)kSecAttrLabel: certAlias + }; + SecItemDelete((__bridge CFDictionaryRef)deleteCertQuery); + + NSDictionary *certAttributes = @{ + (__bridge id)kSecClass: (__bridge id)kSecClassCertificate, + (__bridge id)kSecValueRef: (__bridge id)cert, + (__bridge id)kSecAttrLabel: certAlias + }; + OSStatus status = SecItemAdd((__bridge CFDictionaryRef)certAttributes, NULL); + if (status != errSecSuccess && status != errSecDuplicateItem) { + RCTLogWarn(@"createIdentity: Failed to store certificate, status: %d", (int)status); + CFRelease(cert); + //CFRelease(privateKey); + return NULL; + } + + // Add the private key to keychain + NSDictionary *keyAttributes = @{ + (__bridge id)kSecClass: (__bridge id)kSecClassKey, + (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeRSA, + (__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPrivate, + //(__bridge id)kSecValueData: keyData, + //(__bridge id)kSecReturnPersistentRef: @YES, + //(__bridge id)kSecAttrKeySizeInBits: @(keySize), + //(__bridge id)kSecAttrIsPermanent: @YES, + //(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAfterFirstUnlock, + (__bridge id)kSecAttrLabel: keyAlias, + //(__bridge id)kSecAttrApplicationTag: [kKeychainApplicationTag dataUsingEncoding:NSUTF8StringEncoding], + }; + status = SecItemAdd((__bridge CFDictionaryRef)keyAttributes, NULL); + + if (status != errSecSuccess && status != errSecDuplicateItem) { + RCTLogWarn(@"createIdentity: Failed to store private key, status: %d", (int)status); + CFRelease(cert); + //CFRelease(privateKey); + return NULL; + } + + // Try to retrieve private key from keychain + // NSDictionary *privateKeyQuery = @{ + // (__bridge id)kSecClass: (__bridge id)kSecClassKey, + // (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne, + // (__bridge id)kSecReturnRef: @YES, + // (__bridge id)kSecReturnData: @YES, + // (__bridge id)kSecAttrLabel: @"My PrivateKey", + // //(__bridge id)kSecAttrApplicationTag: [kKeychainApplicationTag dataUsingEncoding:NSUTF8StringEncoding], + // //(__bridge id)kSecUseDataProtectionKeychain: @YES + // }; + // SecKeyRef privateKey = NULL; + // status = SecItemCopyMatching((__bridge CFDictionaryRef)privateKeyQuery, (CFTypeRef *)&privateKey); + + // Add the certificate to keychain + // NSDictionary *certAttributes = @{ + // (__bridge id)kSecClass: (__bridge id)kSecClassCertificate, + // (__bridge id)kSecValueRef: (__bridge id)cert, + // (__bridge id)kSecAttrLabel: kKeychainCertificateLabel + // }; + // + // status = SecItemAdd((__bridge CFDictionaryRef)certAttributes, NULL); + // if (status != errSecSuccess && status != errSecDuplicateItem) { + // RCTLogWarn(@"createIdentity: Failed to store certificate, status: %d", (int)status); + // CFRelease(cert); + // CFRelease(privateKey); + // return NULL; + // } + + // // Query for the identity with correct attributes + // NSDictionary *identityQuery = @{ + // (__bridge id)kSecClass: (__bridge id)kSecClassIdentity, + // (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne, + // (__bridge id)kSecReturnRef: @YES, + // //(__bridge id)kSecAttrLabel: @"My Certificate", + // //(__bridge id)kSecUseDataProtectionKeychain: @YES + // }; + NSDictionary *identityQuery = @{ + (__bridge id)kSecClass: (__bridge id)kSecClassIdentity, + (__bridge id)kSecReturnRef: @YES, + (__bridge id)kSecMatchItemList:@[(__bridge id)cert], + (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne, + //(__bridge id)kSecAttrLabel: @"My Certificate", + //(__bridge id)kSecUseDataProtectionKeychain: @YES + }; + + + // Query for the identity + // NSDictionary *identityQuery = @{ + // (__bridge id)kSecClass: (__bridge id)kSecClassIdentity, + // (__bridge id)kSecReturnRef: @YES, + // (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne + // }; + + SecIdentityRef identity = NULL; + identity = SecIdentityCreate(NULL, cert, privateKey); + //status = SecItemCopyMatching((__bridge CFDictionaryRef)identityQuery, (CFTypeRef *)&identity); + // + // if (status != errSecSuccess || !identity) { + // RCTLogWarn(@"createIdentity: Failed to find identity, status: %d", (int)status); + // } else { + // RCTLogWarn(@"createIdentity: Successfully found identity"); + // } + + // Clean up + CFRelease(cert); + CFRelease(privateKey); + + return identity; +} + +- (NSString *)stripPEMHeader:(NSString *)pemData prefix:(NSString *)prefix { + NSMutableString *cleaned = [pemData mutableCopy]; + + // Remove header + NSString *header = [NSString stringWithFormat:@"-----BEGIN %@-----", prefix]; + [cleaned replaceOccurrencesOfString:header + withString:@"" + options:NSLiteralSearch + range:NSMakeRange(0, cleaned.length)]; + + // Remove footer + NSString *footer = [NSString stringWithFormat:@"-----END %@-----", prefix]; + [cleaned replaceOccurrencesOfString:footer + withString:@"" + options:NSLiteralSearch + range:NSMakeRange(0, cleaned.length)]; + + // Remove whitespace and newlines + NSArray *components = [cleaned componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + NSString *result = [components componentsJoinedByString:@""]; + + return result; +} + +- (NSDictionary *)getPeerCertificate { + if (!_tcpSocket || !_tls || !_peerTrust) { return nil; } - SecCertificateRef certificate = SecTrustGetCertificateAtIndex(trust, 0); + SecCertificateRef certificate = SecTrustGetCertificateAtIndex(_peerTrust, 0); if (!certificate) { return nil; } @@ -679,9 +833,8 @@ - (NSDictionary *)certificateToDict:(SecCertificateRef)certificate detailed:(BOO NSMutableDictionary *certInfo = [NSMutableDictionary dictionary]; // Get public key info - SecKeyRef publicKey = NULL; - OSStatus status = SecCertificateCopyPublicKey(certificate, &publicKey); - if (status == errSecSuccess && publicKey) { + SecKeyRef publicKey = SecCertificateCopyKey(certificate); + if (publicKey) { CFDictionaryRef attributes = SecKeyCopyAttributes(publicKey); if (attributes) { // Get key size (bits) @@ -711,75 +864,83 @@ - (NSDictionary *)certificateToDict:(SecCertificateRef)certificate detailed:(BOO CFRelease(publicKey); } - // Get subject DN - CFDictionaryRef subjectName = SecCertificateCopyNormalizedSubjectContent(certificate, &status); + // Get subject + CFStringRef subjectName = NULL; + OSStatus status = SecCertificateCopyCommonName(certificate, &subjectName); if (status == errSecSuccess && subjectName) { - certInfo[@"subject"] = [self parseDN:(__bridge NSDictionary *)subjectName]; - CFRelease(subjectName); - } - - // Get issuer DN - CFDictionaryRef issuerName = SecCertificateCopyNormalizedIssuerContent(certificate, &status); - if (status == errSecSuccess && issuerName) { - certInfo[@"issuer"] = [self parseDN:(__bridge NSDictionary *)issuerName]; - CFRelease(issuerName); - } - - // Get validity dates - CFDictionaryRef values = NULL; - status = SecCertificateCopyValues(certificate, NULL, &values); - if (status == errSecSuccess && values) { - CFDictionaryRef validityPeriod = CFDictionaryGetValue(values, kSecOIDValidityPeriod); - if (validityPeriod) { - CFArrayRef validityArray = CFDictionaryGetValue(validityPeriod, kSecPropertyKeyValue); - if (validityArray && CFArrayGetCount(validityArray) == 2) { - CFDictionaryRef notBeforeDict = CFArrayGetValueAtIndex(validityArray, 0); - CFDictionaryRef notAfterDict = CFArrayGetValueAtIndex(validityArray, 1); - - CFStringRef notBefore = CFDictionaryGetValue(notBeforeDict, kSecPropertyKeyValue); - CFStringRef notAfter = CFDictionaryGetValue(notAfterDict, kSecPropertyKeyValue); - - if (notBefore) { - NSDate *fromDate = (__bridge NSDate *)notBefore; - certInfo[@"valid_from"] = [self formatDate:fromDate]; - } - if (notAfter) { - NSDate *toDate = (__bridge NSDate *)notAfter; - certInfo[@"valid_to"] = [self formatDate:toDate]; - } - } - } - - // Check if it's a CA - CFDictionaryRef basicConstraints = CFDictionaryGetValue(values, kSecOIDBasicConstraints); - if (basicConstraints) { - CFTypeRef value = CFDictionaryGetValue(basicConstraints, kSecPropertyKeyValue); - if (value) { - certInfo[@"ca"] = @(CFBooleanGetValue(value)); - } - } - - CFRelease(values); + certInfo[@"subject"] = @{@"CN": (__bridge_transfer NSString *)subjectName}; } - // Get serial number - NSData *serialData = (__bridge_transfer NSData *)SecCertificateCopySerialNumber(certificate, NULL); - if (serialData) { - NSMutableString *serialHex = [NSMutableString string]; - const unsigned char *bytes = serialData.bytes; - for (NSInteger i = 0; i < serialData.length; i++) { - [serialHex appendFormat:@"%02X", bytes[i]]; + // Get issuer using the normalized sequence + CFDataRef issuerSequence = SecCertificateCopyNormalizedIssuerSequence(certificate); + if (issuerSequence) { + CFStringRef issuerName = NULL; + status = SecCertificateCopyCommonName(certificate, &issuerName); + if (status == errSecSuccess && issuerName) { + certInfo[@"issuer"] = @{@"CN": (__bridge_transfer NSString *)issuerName}; } - certInfo[@"serialNumber"] = serialHex; - } - - // Get fingerprints - NSData *certData = (__bridge_transfer NSData *)SecCertificateCopyData(certificate); - if (certData) { - certInfo[@"fingerprint"] = [self calculateFingerprint:certData algorithm:CC_SHA1 length:CC_SHA1_DIGEST_LENGTH]; - certInfo[@"fingerprint256"] = [self calculateFingerprint:certData algorithm:CC_SHA256 length:CC_SHA256_DIGEST_LENGTH]; - certInfo[@"fingerprint512"] = [self calculateFingerprint:certData algorithm:CC_SHA512 length:CC_SHA512_DIGEST_LENGTH]; + CFRelease(issuerSequence); } + +// NSData *certData = (__bridge_transfer NSData *)SecCertificateCopyData(certificate); +// if (certData) { +// +// } +// // Get validity dates +// CFDictionaryRef values = NULL; +// status = SecCertificateCopyValues(certificate, NULL, &values); +// if (status == errSecSuccess && values) { +// CFDictionaryRef validityPeriod = CFDictionaryGetValue(values, kSecOIDValidityPeriod); +// if (validityPeriod) { +// CFArrayRef validityArray = CFDictionaryGetValue(validityPeriod, kSecPropertyKeyValue); +// if (validityArray && CFArrayGetCount(validityArray) == 2) { +// CFDictionaryRef notBeforeDict = CFArrayGetValueAtIndex(validityArray, 0); +// CFDictionaryRef notAfterDict = CFArrayGetValueAtIndex(validityArray, 1); +// +// CFStringRef notBefore = CFDictionaryGetValue(notBeforeDict, kSecPropertyKeyValue); +// CFStringRef notAfter = CFDictionaryGetValue(notAfterDict, kSecPropertyKeyValue); +// +// if (notBefore) { +// NSDate *fromDate = (__bridge NSDate *)notBefore; +// certInfo[@"valid_from"] = [self formatDate:fromDate]; +// } +// if (notAfter) { +// NSDate *toDate = (__bridge NSDate *)notAfter; +// certInfo[@"valid_to"] = [self formatDate:toDate]; +// } +// } +// } +// +// // Check if it's a CA +// CFDictionaryRef basicConstraints = CFDictionaryGetValue(values, kSecOIDBasicConstraints); +// if (basicConstraints) { +// CFTypeRef value = CFDictionaryGetValue(basicConstraints, kSecPropertyKeyValue); +// if (value) { +// certInfo[@"ca"] = @(CFBooleanGetValue(value)); +// } +// } +// +// CFRelease(values); +// } +// +// // Get serial number +// NSData *serialData = (__bridge_transfer NSData *)SecCertificateCopySerialNumber(certificate, NULL); +// if (serialData) { +// NSMutableString *serialHex = [NSMutableString string]; +// const unsigned char *bytes = serialData.bytes; +// for (NSInteger i = 0; i < serialData.length; i++) { +// [serialHex appendFormat:@"%02X", bytes[i]]; +// } +// certInfo[@"serialNumber"] = serialHex; +// } +// +// // Get fingerprints +// NSData *certData = (__bridge_transfer NSData *)SecCertificateCopyData(certificate); +// if (certData) { +// certInfo[@"fingerprint"] = [self calculateFingerprint:certData algorithm:CC_SHA1 length:CC_SHA1_DIGEST_LENGTH]; +// certInfo[@"fingerprint256"] = [self calculateFingerprint:certData algorithm:CC_SHA256 length:CC_SHA256_DIGEST_LENGTH]; +// certInfo[@"fingerprint512"] = [self calculateFingerprint:certData algorithm:CC_SHA512 length:CC_SHA512_DIGEST_LENGTH]; +// } return certInfo; } @@ -851,27 +1012,27 @@ - (NSString *)calculateFingerprint:(NSData *)data algorithm:(unsigned char * (^) return [fingerprint substringToIndex:fingerprint.length - 1]; // Remove last colon } -- (NSDictionary *)parseDN:(NSDictionary *)dnDict { - NSMutableDictionary *result = [NSMutableDictionary dictionary]; - - // Map common DN fields - NSDictionary *mapping = @{ - (id)kSecOIDCommonName: @"CN", - (id)kSecOIDDNQualifier: @"dnQualifier" - }; - - for (id key in dnDict) { - NSString *mappedKey = mapping[key]; - if (mappedKey) { - NSString *value = dnDict[key]; - if ([value isKindOfClass:[NSString class]]) { - result[mappedKey] = value; - } - } - } - - return result; -} +//- (NSDictionary *)parseDN:(NSDictionary *)dnDict { +// NSMutableDictionary *result = [NSMutableDictionary dictionary]; +// +// // Map common DN fields +// NSDictionary *mapping = @{ +// (id)kSecOIDCommonName: @"CN", +// (id)kSecOIDDNQualifier: @"dnQualifier" +// }; +// +// for (id key in dnDict) { +// NSString *mappedKey = mapping[key]; +// if (mappedKey) { +// NSString *value = dnDict[key]; +// if ([value isKindOfClass:[NSString class]]) { +// result[mappedKey] = value; +// } +// } +// } +// +// return result; +//} - (NSString *)formatDate:(NSDate *)date { NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; From c6c7d6dead28dbaf69ef45711ff9a5ac93c0103d Mon Sep 17 00:00:00 2001 From: Vince Ricosti <80467769+vricosti@users.noreply.github.com> Date: Tue, 28 Jan 2025 14:53:32 +0100 Subject: [PATCH 05/11] Clean code --- ios/TcpSocketClient.m | 239 +++++++++--------------------------------- ios/TcpSockets.m | 7 +- 2 files changed, 51 insertions(+), 195 deletions(-) diff --git a/ios/TcpSocketClient.m b/ios/TcpSocketClient.m index b5d48c4..5a26b57 100644 --- a/ios/TcpSocketClient.m +++ b/ios/TcpSocketClient.m @@ -597,11 +597,13 @@ - (SecIdentityRef)createIdentityWithCert:(NSString *)pemCert settings:(NSDictionary *)settings { RCTLogWarn(@"createIdentity: Starting identity creation"); + SecIdentityRef identity = NULL; + // Strip PEM headers and convert to data NSString *cleanedCert = [self stripPEMHeader:pemCert prefix:@"CERTIFICATE"]; NSString *cleanedPrivateKey = [self stripPEMHeader:pemKey prefix:@"PRIVATE KEY"]; NSString *certAlias = settings[@"certAlias"]; - NSString *keyAlias= settings[@"keyAlias"]; + NSString *keyAlias = certAlias; // On iOS we must use the same label to create an identity NSData *certData = [[NSData alloc] initWithBase64EncodedString:cleanedCert options:NSDataBase64DecodingIgnoreUnknownCharacters]; @@ -620,18 +622,17 @@ - (SecIdentityRef)createIdentityWithCert:(NSString *)pemCert return NULL; } - // Extract RSA key from PKCS#8 - TODO use a ASN1 decoder to detect the key format ... - // For my own use I know it's a pem but I prefer not to trust a file extension and - // it's better to check from asn1 data + // Extract RSA key from PKCS#8 + // For my own use I know it's a pem but it would be better to not trust a file extension and + // TODO use a ASN1 decoder to detect the key format ... NSData *rsaKeyData = [self extractRSAKeyFromPKCS8:pkcs8KeyData]; if (!rsaKeyData) { RCTLogWarn(@"Failed to extract RSA key from PKCS#8"); CFRelease(cert); return NULL; } - + NSDictionary *privateKeyAttributes = @{ - //(__bridge id)kSecClass: (__bridge id)kSecClassKey, (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeRSA, (__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPrivate, //(__bridge id)kSecAttrLabel: kKeychainPrivateKeyLabel, @@ -655,35 +656,11 @@ - (SecIdentityRef)createIdentityWithCert:(NSString *)pemCert }; SecItemDelete((__bridge CFDictionaryRef)deleteKeyQuery); - // Import certificate in keychain - NSDictionary *deleteCertQuery = @{ - (__bridge id)kSecClass: (__bridge id)kSecClassCertificate, - (__bridge id)kSecAttrLabel: certAlias - }; - SecItemDelete((__bridge CFDictionaryRef)deleteCertQuery); - - NSDictionary *certAttributes = @{ - (__bridge id)kSecClass: (__bridge id)kSecClassCertificate, - (__bridge id)kSecValueRef: (__bridge id)cert, - (__bridge id)kSecAttrLabel: certAlias - }; - OSStatus status = SecItemAdd((__bridge CFDictionaryRef)certAttributes, NULL); - if (status != errSecSuccess && status != errSecDuplicateItem) { - RCTLogWarn(@"createIdentity: Failed to store certificate, status: %d", (int)status); - CFRelease(cert); - //CFRelease(privateKey); - return NULL; - } - // Add the private key to keychain NSDictionary *keyAttributes = @{ (__bridge id)kSecClass: (__bridge id)kSecClassKey, (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeRSA, (__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPrivate, - //(__bridge id)kSecValueData: keyData, - //(__bridge id)kSecReturnPersistentRef: @YES, - //(__bridge id)kSecAttrKeySizeInBits: @(keySize), - //(__bridge id)kSecAttrIsPermanent: @YES, //(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAfterFirstUnlock, (__bridge id)kSecAttrLabel: keyAlias, //(__bridge id)kSecAttrApplicationTag: [kKeychainApplicationTag dataUsingEncoding:NSUTF8StringEncoding], @@ -693,72 +670,52 @@ - (SecIdentityRef)createIdentityWithCert:(NSString *)pemCert if (status != errSecSuccess && status != errSecDuplicateItem) { RCTLogWarn(@"createIdentity: Failed to store private key, status: %d", (int)status); CFRelease(cert); - //CFRelease(privateKey); + CFRelease(privateKey); return NULL; } + + // Import certificate in keychain + NSDictionary *deleteCertQuery = @{ + (__bridge id)kSecClass: (__bridge id)kSecClassCertificate, + (__bridge id)kSecAttrLabel: certAlias + }; + SecItemDelete((__bridge CFDictionaryRef)deleteCertQuery); - // Try to retrieve private key from keychain - // NSDictionary *privateKeyQuery = @{ - // (__bridge id)kSecClass: (__bridge id)kSecClassKey, - // (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne, - // (__bridge id)kSecReturnRef: @YES, - // (__bridge id)kSecReturnData: @YES, - // (__bridge id)kSecAttrLabel: @"My PrivateKey", - // //(__bridge id)kSecAttrApplicationTag: [kKeychainApplicationTag dataUsingEncoding:NSUTF8StringEncoding], - // //(__bridge id)kSecUseDataProtectionKeychain: @YES - // }; - // SecKeyRef privateKey = NULL; - // status = SecItemCopyMatching((__bridge CFDictionaryRef)privateKeyQuery, (CFTypeRef *)&privateKey); - - // Add the certificate to keychain - // NSDictionary *certAttributes = @{ - // (__bridge id)kSecClass: (__bridge id)kSecClassCertificate, - // (__bridge id)kSecValueRef: (__bridge id)cert, - // (__bridge id)kSecAttrLabel: kKeychainCertificateLabel - // }; - // - // status = SecItemAdd((__bridge CFDictionaryRef)certAttributes, NULL); - // if (status != errSecSuccess && status != errSecDuplicateItem) { - // RCTLogWarn(@"createIdentity: Failed to store certificate, status: %d", (int)status); - // CFRelease(cert); - // CFRelease(privateKey); - // return NULL; - // } - - // // Query for the identity with correct attributes - // NSDictionary *identityQuery = @{ - // (__bridge id)kSecClass: (__bridge id)kSecClassIdentity, - // (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne, - // (__bridge id)kSecReturnRef: @YES, - // //(__bridge id)kSecAttrLabel: @"My Certificate", - // //(__bridge id)kSecUseDataProtectionKeychain: @YES - // }; - NSDictionary *identityQuery = @{ - (__bridge id)kSecClass: (__bridge id)kSecClassIdentity, - (__bridge id)kSecReturnRef: @YES, - (__bridge id)kSecMatchItemList:@[(__bridge id)cert], - (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne, - //(__bridge id)kSecAttrLabel: @"My Certificate", - //(__bridge id)kSecUseDataProtectionKeychain: @YES + NSDictionary *certAttributes = @{ + (__bridge id)kSecClass: (__bridge id)kSecClassCertificate, + (__bridge id)kSecValueRef: (__bridge id)cert, + (__bridge id)kSecAttrLabel: certAlias }; + OSStatus status = SecItemAdd((__bridge CFDictionaryRef)certAttributes, NULL); + if (status != errSecSuccess && status != errSecDuplicateItem) { + RCTLogWarn(@"createIdentity: Failed to store certificate, status: %d", (int)status); + CFRelease(cert); + CFRelease(privateKey); + return NULL; + } - // Query for the identity - // NSDictionary *identityQuery = @{ - // (__bridge id)kSecClass: (__bridge id)kSecClassIdentity, - // (__bridge id)kSecReturnRef: @YES, - // (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne - // }; - SecIdentityRef identity = NULL; - identity = SecIdentityCreate(NULL, cert, privateKey); - //status = SecItemCopyMatching((__bridge CFDictionaryRef)identityQuery, (CFTypeRef *)&identity); - // - // if (status != errSecSuccess || !identity) { - // RCTLogWarn(@"createIdentity: Failed to find identity, status: %d", (int)status); - // } else { - // RCTLogWarn(@"createIdentity: Successfully found identity"); - // } + //------ PRIVATE API: need to find the proper way of doing it ------- + if (NO) { + identity = SecIdentityCreate(NULL, cert, privateKey); + } else { + NSDictionary *identityQuery = @{ + (__bridge id)kSecClass: (__bridge id)kSecClassIdentity, + (__bridge id)kSecReturnRef: @YES, + //(__bridge id)kSecMatchItemList:@[(__bridge id)cert], + (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne, + (__bridge id)kSecAttrLabel: certAlias, + //(__bridge id)kSecUseDataProtectionKeychain: @YES + }; + status = SecItemCopyMatching((__bridge CFDictionaryRef)identityQuery, (CFTypeRef *)&identity); + + if (status != errSecSuccess || !identity) { + RCTLogWarn(@"createIdentity: Failed to find identity, status: %d", (int)status); + } else { + RCTLogWarn(@"createIdentity: Successfully found identity"); + } + } // Clean up CFRelease(cert); @@ -829,6 +786,9 @@ - (NSDictionary *)getCertificate { return result; } +// We need an ASN1 decoder to parse properly but for my case I only need modulus and exponent +// In addition SecCertificateCopyNormalizedIssuerSequence has some issues since it normalizes +// issuer and we don't want that - (NSDictionary *)certificateToDict:(SecCertificateRef)certificate detailed:(BOOL)detailed { NSMutableDictionary *certInfo = [NSMutableDictionary dictionary]; @@ -882,66 +842,6 @@ - (NSDictionary *)certificateToDict:(SecCertificateRef)certificate detailed:(BOO CFRelease(issuerSequence); } -// NSData *certData = (__bridge_transfer NSData *)SecCertificateCopyData(certificate); -// if (certData) { -// -// } -// // Get validity dates -// CFDictionaryRef values = NULL; -// status = SecCertificateCopyValues(certificate, NULL, &values); -// if (status == errSecSuccess && values) { -// CFDictionaryRef validityPeriod = CFDictionaryGetValue(values, kSecOIDValidityPeriod); -// if (validityPeriod) { -// CFArrayRef validityArray = CFDictionaryGetValue(validityPeriod, kSecPropertyKeyValue); -// if (validityArray && CFArrayGetCount(validityArray) == 2) { -// CFDictionaryRef notBeforeDict = CFArrayGetValueAtIndex(validityArray, 0); -// CFDictionaryRef notAfterDict = CFArrayGetValueAtIndex(validityArray, 1); -// -// CFStringRef notBefore = CFDictionaryGetValue(notBeforeDict, kSecPropertyKeyValue); -// CFStringRef notAfter = CFDictionaryGetValue(notAfterDict, kSecPropertyKeyValue); -// -// if (notBefore) { -// NSDate *fromDate = (__bridge NSDate *)notBefore; -// certInfo[@"valid_from"] = [self formatDate:fromDate]; -// } -// if (notAfter) { -// NSDate *toDate = (__bridge NSDate *)notAfter; -// certInfo[@"valid_to"] = [self formatDate:toDate]; -// } -// } -// } -// -// // Check if it's a CA -// CFDictionaryRef basicConstraints = CFDictionaryGetValue(values, kSecOIDBasicConstraints); -// if (basicConstraints) { -// CFTypeRef value = CFDictionaryGetValue(basicConstraints, kSecPropertyKeyValue); -// if (value) { -// certInfo[@"ca"] = @(CFBooleanGetValue(value)); -// } -// } -// -// CFRelease(values); -// } -// -// // Get serial number -// NSData *serialData = (__bridge_transfer NSData *)SecCertificateCopySerialNumber(certificate, NULL); -// if (serialData) { -// NSMutableString *serialHex = [NSMutableString string]; -// const unsigned char *bytes = serialData.bytes; -// for (NSInteger i = 0; i < serialData.length; i++) { -// [serialHex appendFormat:@"%02X", bytes[i]]; -// } -// certInfo[@"serialNumber"] = serialHex; -// } -// -// // Get fingerprints -// NSData *certData = (__bridge_transfer NSData *)SecCertificateCopyData(certificate); -// if (certData) { -// certInfo[@"fingerprint"] = [self calculateFingerprint:certData algorithm:CC_SHA1 length:CC_SHA1_DIGEST_LENGTH]; -// certInfo[@"fingerprint256"] = [self calculateFingerprint:certData algorithm:CC_SHA256 length:CC_SHA256_DIGEST_LENGTH]; -// certInfo[@"fingerprint512"] = [self calculateFingerprint:certData algorithm:CC_SHA512 length:CC_SHA512_DIGEST_LENGTH]; -// } - return certInfo; } @@ -1001,47 +901,6 @@ - (NSArray *)parseRSAPublicKey:(NSData *)keyData { return @[modulus, exponent]; } -- (NSString *)calculateFingerprint:(NSData *)data algorithm:(unsigned char * (^)(const void *, CC_LONG, unsigned char *))hashFunction length:(CC_LONG)hashLength { - unsigned char digest[hashLength]; - hashFunction(data.bytes, (CC_LONG)data.length, digest); - - NSMutableString *fingerprint = [NSMutableString stringWithCapacity:hashLength * 3]; - for (int i = 0; i < hashLength; i++) { - [fingerprint appendFormat:@"%02X:", digest[i]]; - } - return [fingerprint substringToIndex:fingerprint.length - 1]; // Remove last colon -} - -//- (NSDictionary *)parseDN:(NSDictionary *)dnDict { -// NSMutableDictionary *result = [NSMutableDictionary dictionary]; -// -// // Map common DN fields -// NSDictionary *mapping = @{ -// (id)kSecOIDCommonName: @"CN", -// (id)kSecOIDDNQualifier: @"dnQualifier" -// }; -// -// for (id key in dnDict) { -// NSString *mappedKey = mapping[key]; -// if (mappedKey) { -// NSString *value = dnDict[key]; -// if ([value isKindOfClass:[NSString class]]) { -// result[mappedKey] = value; -// } -// } -// } -// -// return result; -//} - -- (NSString *)formatDate:(NSDate *)date { - NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; - formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; - formatter.dateFormat = @"MMM dd HH:mm:ss yyyy 'GMT'"; - formatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"]; - return [formatter stringFromDate:date]; -} - - (NSError *)badInvocationError:(NSString *)errMsg { NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg diff --git a/ios/TcpSockets.m b/ios/TcpSockets.m index a689ada..3aca377 100644 --- a/ios/TcpSockets.m +++ b/ios/TcpSockets.m @@ -175,9 +175,7 @@ - (TcpSocketClient *)createSocket:(nonnull NSNumber *)cId { [client resume]; } -// Method with Promise (modern style) RCT_EXPORT_METHOD(getPeerCertificate:(nonnull NSNumber *)cId - detailed:(BOOL)detailed resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { TcpSocketClient *client = [self findClient:cId]; @@ -186,12 +184,11 @@ - (TcpSocketClient *)createSocket:(nonnull NSNumber *)cId { return; } - NSDictionary *cert = [client getPeerCertificate:detailed]; + NSDictionary *cert = [client getPeerCertificate]; resolve(cert ?: [NSNull null]); } RCT_EXPORT_METHOD(getCertificate:(nonnull NSNumber *)cId - detailed:(BOOL)detailed resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { TcpSocketClient *client = [self findClient:cId]; @@ -200,7 +197,7 @@ - (TcpSocketClient *)createSocket:(nonnull NSNumber *)cId { return; } - NSDictionary *cert = [client getCertificate:detailed]; + NSDictionary *cert = [client getCertificate]; resolve(cert ?: [NSNull null]); } From 35d5e0420cf61124972b19461c6b6ebac073d3f1 Mon Sep 17 00:00:00 2001 From: Vince Ricosti <80467769+vricosti@users.noreply.github.com> Date: Sat, 1 Feb 2025 15:44:37 +0100 Subject: [PATCH 06/11] fix a bug when pkcs1 is passed --- ios/TcpSocketClient.m | 494 ++++++++++++++++++++++++++---------------- 1 file changed, 310 insertions(+), 184 deletions(-) diff --git a/ios/TcpSocketClient.m b/ios/TcpSocketClient.m index 5a26b57..0853583 100644 --- a/ios/TcpSocketClient.m +++ b/ios/TcpSocketClient.m @@ -6,18 +6,19 @@ #import #import -#import #import +#import #import #import #import -#import +#import NSString *const RCTTCPErrorDomain = @"RCTTCPErrorDomain"; @implementation ResolvableOption -- (instancetype)initWithValue:(NSString *)value needsResolution:(BOOL)needsResolution { +- (instancetype)initWithValue:(NSString *)value + needsResolution:(BOOL)needsResolution { if (self = [super init]) { _value = value; _needsResolution = needsResolution; @@ -25,7 +26,8 @@ - (instancetype)initWithValue:(NSString *)value needsResolution:(BOOL)needsResol return self; } -+ (instancetype)optionWithValue:(NSString *)value needsResolution:(BOOL)needsResolution { ++ (instancetype)optionWithValue:(NSString *)value + needsResolution:(BOOL)needsResolution { return [[self alloc] initWithValue:value needsResolution:needsResolution]; } @@ -33,12 +35,13 @@ - (NSString *)resolve { if (!self.needsResolution) { return self.value; } - + NSURL *url = [[NSURL alloc] initWithString:self.value]; NSError *error = nil; - NSString *contents = [[NSString alloc] initWithContentsOfURL:url - encoding:NSUTF8StringEncoding - error:&error]; + NSString *contents = + [[NSString alloc] initWithContentsOfURL:url + encoding:NSUTF8StringEncoding + error:&error]; if (error) { return nil; } @@ -91,7 +94,10 @@ + (id)socketClientWithId:(nonnull NSNumber *)clientID - (id)initWithClientId:(NSNumber *)clientID andConfig:(id)aDelegate { - return [self initWithClientId:clientID andConfig:aDelegate andSocket:nil andServer:nil]; + return [self initWithClientId:clientID + andConfig:aDelegate + andSocket:nil + andServer:nil]; } - (id)initWithClientId:(NSNumber *)clientID @@ -163,34 +169,38 @@ - (BOOL)connect:(NSString *)host return result; } -- (ResolvableOption *)getResolvableOption:(NSDictionary *)tlsOptions forKey:(NSString *)key { +- (ResolvableOption *)getResolvableOption:(NSDictionary *)tlsOptions + forKey:(NSString *)key { if (![tlsOptions objectForKey:key]) { return nil; } - + NSString *value = tlsOptions[key]; NSArray *resolvedKeys = tlsOptions[@"resolvedKeys"]; - BOOL needsResolution = resolvedKeys != nil && [resolvedKeys containsObject:key]; - - return [ResolvableOption optionWithValue:value needsResolution:needsResolution]; + BOOL needsResolution = + resolvedKeys != nil && [resolvedKeys containsObject:key]; + + return [ResolvableOption optionWithValue:value + needsResolution:needsResolution]; } - (void)startTLS:(NSDictionary *)tlsOptions { - if (_tls) return; - NSMutableDictionary *settings = [NSMutableDictionary dictionary]; + if (_tls) + return; + _tlsSettings = [NSMutableDictionary dictionary]; _resolvableCaCert = [self getResolvableOption:tlsOptions forKey:@"ca"]; _resolvableKey = [self getResolvableOption:tlsOptions forKey:@"key"]; _resolvableCert = [self getResolvableOption:tlsOptions forKey:@"cert"]; NSString *keyAlias = tlsOptions[@"keyAlias"] ?: @"key"; - [settings setObject:keyAlias forKey:@"keyAlias"]; + [(NSMutableDictionary *)_tlsSettings setObject:keyAlias forKey:@"keyAlias"]; NSString *certAlias = tlsOptions[@"certAlias"] ?: @"cert"; - [settings setObject:certAlias forKey:@"certAlias"]; - - RCTLogWarn(@"startTLS: Resolved options - CA: %@, Key exists: %@, Cert exists: %@", - _resolvableCaCert ? @"YES" : @"NO", - _resolvableKey ? @"YES" : @"NO", - _resolvableCert ? @"YES" : @"NO"); - + [(NSMutableDictionary *)_tlsSettings setObject:certAlias forKey:@"certAlias"]; + + RCTLogWarn( + @"startTLS: Resolved options - CA: %@, Key exists: %@, Cert exists: %@", + _resolvableCaCert ? @"YES" : @"NO", _resolvableKey ? @"YES" : @"NO", + _resolvableCert ? @"YES" : @"NO"); + BOOL checkValidity = (tlsOptions[@"rejectUnauthorized"] ? [tlsOptions[@"rejectUnauthorized"] boolValue] : true); @@ -198,17 +208,19 @@ - (void)startTLS:(NSDictionary *)tlsOptions { // Do not validate RCTLogWarn(@"startTLS: Validation disabled"); _checkValidity = false; - [settings setObject:[NSNumber numberWithBool:YES] + [(NSMutableDictionary *)_tlsSettings setObject:[NSNumber numberWithBool:YES] forKey:GCDAsyncSocketManuallyEvaluateTrust]; } else if (_resolvableCaCert != nil) { // Self-signed certificate RCTLogWarn(@"startTLS: Using self-signed certificate validation"); - [settings setObject:[NSNumber numberWithBool:YES] + [(NSMutableDictionary *)_tlsSettings setObject:[NSNumber numberWithBool:YES] forKey:GCDAsyncSocketManuallyEvaluateTrust]; } else { // Default certificates - RCTLogWarn(@"startTLS: Using default certificate validation with host: %@", _host); - [settings setObject:_host forKey:(NSString *)kCFStreamSSLPeerName]; + RCTLogWarn( + @"startTLS: Using default certificate validation with host: %@", + _host); + [(NSMutableDictionary *)_tlsSettings setObject:_host forKey:(NSString *)kCFStreamSSLPeerName]; } // Handle client certificate authentication @@ -216,28 +228,34 @@ - (void)startTLS:(NSDictionary *)tlsOptions { RCTLogWarn(@"startTLS: Attempting client certificate authentication"); NSString *pemCert = [_resolvableCert resolve]; NSString *pemKey = [_resolvableKey resolve]; - - RCTLogWarn(@"startTLS: Resolved PEM cert exists: %@, PEM key exists: %@", - pemCert ? @"YES" : @"NO", - pemKey ? @"YES" : @"NO"); - + + RCTLogWarn( + @"startTLS: Resolved PEM cert exists: %@, PEM key exists: %@", + pemCert ? @"YES" : @"NO", pemKey ? @"YES" : @"NO"); + if (pemCert && pemKey) { - SecIdentityRef identity = [self createIdentityWithCert:pemCert privateKey:pemKey settings:settings]; - RCTLogWarn(@"startTLS: Identity creation %@", identity ? @"successful" : @"failed"); + SecIdentityRef identity = [self createIdentityWithCert:pemCert + privateKey:pemKey + settings:_tlsSettings]; + RCTLogWarn(@"startTLS: Identity creation %@", + identity ? @"successful" : @"failed"); if (identity) { - NSArray *certificates = @[(__bridge id)identity]; - [settings setObject:certificates forKey:(NSString *)kCFStreamSSLCertificates]; + NSArray *certificates = @[ (__bridge id)identity ]; + [(NSMutableDictionary *)_tlsSettings setObject:certificates + forKey:(NSString *)kCFStreamSSLCertificates]; CFRelease(identity); - RCTLogWarn(@"startTLS: Client certificates configured successfully"); + RCTLogWarn( + @"startTLS: Client certificates configured successfully"); } else { - RCTLogWarn(@"startTLS: Failed to create identity from cert and key"); + RCTLogWarn( + @"startTLS: Failed to create identity from cert and key"); } } } - RCTLogWarn(@"startTLS: Final settings: %@", settings); + RCTLogWarn(@"startTLS: Final settings: %@", _tlsSettings); _tls = true; - [_tcpSocket startTLS:settings]; + [_tcpSocket startTLS:_tlsSettings]; } - (NSDictionary *)getAddress { @@ -339,8 +357,7 @@ - (BOOL)setSecureContext:(NSDictionary *)tlsOptions { CFStringRef password = CFSTR(""); const void *keys[] = {kSecImportExportPassphrase}; const void *values[] = {password}; - CFDictionaryRef options = - CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL); + CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL); CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL); @@ -360,14 +377,18 @@ - (BOOL)setSecureContext:(NSDictionary *)tlsOptions { CFArrayRef myCerts = CFArrayCreate(NULL, (void *)certArray, 1, NULL); _tlsSettings = [NSMutableDictionary dictionary]; - [(NSMutableDictionary*) _tlsSettings setObject:[NSNumber numberWithBool:YES] - forKey:(NSString *)kCFStreamSSLIsServer]; - [(NSMutableDictionary*) _tlsSettings setObject:[NSNumber numberWithInteger:2] - forKey:GCDAsyncSocketSSLProtocolVersionMin]; - [(NSMutableDictionary*) _tlsSettings setObject:[NSNumber numberWithInteger:8] - forKey:GCDAsyncSocketSSLProtocolVersionMax]; - [(NSMutableDictionary*) _tlsSettings setObject:(id)CFBridgingRelease(myCerts) - forKey:(NSString *)kCFStreamSSLCertificates]; + [(NSMutableDictionary *)_tlsSettings + setObject:[NSNumber numberWithBool:YES] + forKey:(NSString *)kCFStreamSSLIsServer]; + [(NSMutableDictionary *)_tlsSettings + setObject:[NSNumber numberWithInteger:2] + forKey:GCDAsyncSocketSSLProtocolVersionMin]; + [(NSMutableDictionary *)_tlsSettings + setObject:[NSNumber numberWithInteger:8] + forKey:GCDAsyncSocketSSLProtocolVersionMax]; + [(NSMutableDictionary *)_tlsSettings + setObject:(id)CFBridgingRelease(myCerts) + forKey:(NSString *)kCFStreamSSLCertificates]; _tls = true; return true; } @@ -485,13 +506,13 @@ - (void)socketDidSecure:(GCDAsyncSocket *)sock { - (void)socket:(GCDAsyncSocket *)sock didReceiveTrust:(SecTrustRef)trust completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler { - + // Store the trust reference if (_peerTrust) { CFRelease(_peerTrust); } _peerTrust = (SecTrustRef)CFRetain(trust); - + // Check if we should check the validity if (!_checkValidity) { completionHandler(YES); @@ -515,9 +536,12 @@ - (void)socket:(GCDAsyncSocket *)sock } // Strip PEM header and footer - NSString *cleanedCaCert = [self stripPEMHeader:pemCaCert prefix:@"CERTIFICATE"]; - NSData *pemCaCertData = [[NSData alloc] initWithBase64EncodedString:cleanedCaCert - options:NSDataBase64DecodingIgnoreUnknownCharacters]; + NSString *cleanedCaCert = [self stripPEMHeader:pemCaCert + prefix:@"CERTIFICATE"]; + NSData *pemCaCertData = [[NSData alloc] + initWithBase64EncodedString:cleanedCaCert + options: + NSDataBase64DecodingIgnoreUnknownCharacters]; SecCertificateRef localCertificate = SecCertificateCreateWithData(NULL, (CFDataRef)pemCaCertData); @@ -579,8 +603,73 @@ - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err { } // https://stackoverflow.com/questions/45997841/how-to-get-a-secidentityref-from-a-seccertificateref-and-a-seckeyref -// We use a private API call here; it's good enough for WebKit. -SecIdentityRef SecIdentityCreate(CFAllocatorRef allocator, SecCertificateRef certificate, SecKeyRef privateKey); +// We use a private API call here. +SecIdentityRef SecIdentityCreate(CFAllocatorRef allocator, + SecCertificateRef certificate, + SecKeyRef privateKey); + +typedef NS_ENUM(NSInteger, PEMType) { + PEMTypeUnknown, + PEMTypeCertificate, + PEMTypePKCS1, + PEMTypePKCS8 +}; + +- (PEMType)detectPEMType:(NSString *)pemData { + if ([pemData containsString:@"BEGIN CERTIFICATE"]) { + return PEMTypeCertificate; + } + if ([pemData containsString:@"BEGIN PRIVATE KEY"]) { + return PEMTypePKCS8; + } + if ([pemData containsString:@"BEGIN RSA PRIVATE KEY"]) { + return PEMTypePKCS1; + } + return PEMTypeUnknown; +} + +- (NSData *)getDataFromPEM:(NSString *)pemData error:(NSError **)error { + PEMType type = [self detectPEMType:pemData]; + if (type == PEMTypeUnknown) { + if (error) { + *error = [NSError + errorWithDomain:RCTTCPErrorDomain + code:-1 + userInfo:@{ + NSLocalizedDescriptionKey : @"Invalid PEM format" + }]; + } + return nil; + } + + // Extract the base64 data between headers + NSString *prefix; + switch (type) { + case PEMTypeCertificate: + prefix = @"CERTIFICATE"; + break; + case PEMTypePKCS8: + prefix = @"PRIVATE KEY"; + break; + case PEMTypePKCS1: + prefix = @"RSA PRIVATE KEY"; + break; + default: + return nil; + } + + NSString *cleanedPEM = [self stripPEMHeader:pemData prefix:prefix]; + NSData *decodedData = [[NSData alloc] + initWithBase64EncodedString:cleanedPEM + options: + NSDataBase64DecodingIgnoreUnknownCharacters]; + // For PKCS#8, extract the RSA key + if (type == PEMTypePKCS8) { + return [self extractRSAKeyFromPKCS8:decodedData]; + } + + return decodedData; +} - (NSData *)extractRSAKeyFromPKCS8:(NSData *)pkcs8Data { // PKCS#8 format has a 26-byte header before the RSA private key @@ -596,217 +685,239 @@ - (SecIdentityRef)createIdentityWithCert:(NSString *)pemCert privateKey:(NSString *)pemKey settings:(NSDictionary *)settings { RCTLogWarn(@"createIdentity: Starting identity creation"); - + + OSStatus status = -1; SecIdentityRef identity = NULL; - - // Strip PEM headers and convert to data - NSString *cleanedCert = [self stripPEMHeader:pemCert prefix:@"CERTIFICATE"]; - NSString *cleanedPrivateKey = [self stripPEMHeader:pemKey prefix:@"PRIVATE KEY"]; + NSData *publicKeyHash = nil; + + // Get aliases from settings NSString *certAlias = settings[@"certAlias"]; - NSString *keyAlias = certAlias; // On iOS we must use the same label to create an identity - - NSData *certData = [[NSData alloc] initWithBase64EncodedString:cleanedCert - options:NSDataBase64DecodingIgnoreUnknownCharacters]; - NSData *pkcs8KeyData = [[NSData alloc] initWithBase64EncodedString:cleanedPrivateKey - options:NSDataBase64DecodingIgnoreUnknownCharacters]; - - if (!certData || !pkcs8KeyData) { - RCTLogWarn(@"createIdentity: Failed to create data from base64"); + NSString *keyAlias = settings[@"keyAlias"]; + + NSError *pemError = nil; + NSData *certData = [self getDataFromPEM:pemCert error:&pemError]; + NSData *keyData = [self getDataFromPEM:pemKey error:&pemError]; + + if (pemError || !certData || !keyData) { + RCTLogWarn(@"createIdentity: Failed to process PEM data: %@", pemError); return NULL; } - + // Creates a certificate object from its DER representation SecCertificateRef cert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData); if (!cert) { RCTLogWarn(@"createIdentity: Failed to create certificate from data"); return NULL; } - - // Extract RSA key from PKCS#8 - // For my own use I know it's a pem but it would be better to not trust a file extension and - // TODO use a ASN1 decoder to detect the key format ... - NSData *rsaKeyData = [self extractRSAKeyFromPKCS8:pkcs8KeyData]; - if (!rsaKeyData) { - RCTLogWarn(@"Failed to extract RSA key from PKCS#8"); + // Import certificate in keychain + NSDictionary *deleteCertQuery = @{ + (__bridge id)kSecClass : (__bridge id)kSecClassCertificate, + //(__bridge id)kSecAttrLabel: certAlias + }; + status = SecItemDelete((__bridge CFDictionaryRef)deleteCertQuery); + + NSDictionary *certAttributes = @{ + (__bridge id)kSecClass : (__bridge id)kSecClassCertificate, + (__bridge id)kSecValueRef : (__bridge id)cert, + (__bridge id)kSecAttrLabel : certAlias, + }; + status = SecItemAdd((__bridge CFDictionaryRef)certAttributes, NULL); + if (status != errSecSuccess && status != errSecDuplicateItem) { + RCTLogWarn(@"createIdentity: Failed to store certificate, status: %d", + (int)status); CFRelease(cert); return NULL; } + // Retrieve pkhh and use it to set kSecAttrApplicationLabel when importing + // private key + CFDictionaryRef attributesRef = NULL; + NSDictionary *attributes = nil; + NSDictionary *findCertAttributes = @{ + (__bridge id)kSecClass : (__bridge id)kSecClassCertificate, + (__bridge id)kSecReturnAttributes : @YES + }; + status = SecItemCopyMatching((__bridge CFDictionaryRef)findCertAttributes, + (CFTypeRef *)&attributesRef); + if (status == errSecSuccess && attributesRef) { + attributes = (__bridge_transfer NSDictionary *)attributesRef; + NSLog(@"Cert Attributes: %@", attributes); + publicKeyHash = attributes[(__bridge id)kSecAttrPublicKeyHash]; + RCTLogWarn(@"Retrieved public key hash from certificate"); + } NSDictionary *privateKeyAttributes = @{ - (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeRSA, - (__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPrivate, - //(__bridge id)kSecAttrLabel: kKeychainPrivateKeyLabel, - //(__bridge id)kSecAttrApplicationTag: [kKeychainApplicationTag dataUsingEncoding:NSUTF8StringEncoding], + (__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeRSA, + (__bridge id)kSecAttrKeyClass : (__bridge id)kSecAttrKeyClassPrivate, + (__bridge id)kSecReturnPersistentRef : @YES }; - CFErrorRef error = NULL; - SecKeyRef privateKey = SecKeyCreateWithData((__bridge CFDataRef)rsaKeyData, - (__bridge CFDictionaryRef)privateKeyAttributes, - &error); + SecKeyRef privateKey = SecKeyCreateWithData( + (__bridge CFDataRef)keyData, + (__bridge CFDictionaryRef)privateKeyAttributes, &error); if (!privateKey) { RCTLogWarn(@"createIdentity: Failed to create private key: %@", error); CFRelease(cert); - if (error) CFRelease(error); + if (error) + CFRelease(error); return NULL; } - + NSDictionary *deleteKeyQuery = @{ - (__bridge id)kSecClass: (__bridge id)kSecClassKey, - (__bridge id)kSecAttrLabel: keyAlias + (__bridge id)kSecClass : (__bridge id)kSecClassKey, + (__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeRSA, }; - SecItemDelete((__bridge CFDictionaryRef)deleteKeyQuery); - + status = SecItemDelete((__bridge CFDictionaryRef)deleteKeyQuery); + // Add the private key to keychain NSDictionary *keyAttributes = @{ - (__bridge id)kSecClass: (__bridge id)kSecClassKey, - (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeRSA, - (__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPrivate, - //(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAfterFirstUnlock, - (__bridge id)kSecAttrLabel: keyAlias, - //(__bridge id)kSecAttrApplicationTag: [kKeychainApplicationTag dataUsingEncoding:NSUTF8StringEncoding], + (__bridge id)kSecClass : (__bridge id)kSecClassKey, + (__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeRSA, + (__bridge id)kSecAttrKeyClass : (__bridge id)kSecAttrKeyClassPrivate, + (__bridge id)kSecAttrIsPermanent : @"YES", + (__bridge id)kSecAttrLabel : keyAlias, + (__bridge id)kSecAttrApplicationLabel : publicKeyHash }; + status = SecItemAdd((__bridge CFDictionaryRef)keyAttributes, NULL); - if (status != errSecSuccess && status != errSecDuplicateItem) { - RCTLogWarn(@"createIdentity: Failed to store private key, status: %d", (int)status); + RCTLogWarn(@"createIdentity: Failed to store private key, status: %d", + (int)status); CFRelease(cert); CFRelease(privateKey); return NULL; } - // Import certificate in keychain - NSDictionary *deleteCertQuery = @{ - (__bridge id)kSecClass: (__bridge id)kSecClassCertificate, - (__bridge id)kSecAttrLabel: certAlias - }; - SecItemDelete((__bridge CFDictionaryRef)deleteCertQuery); - - NSDictionary *certAttributes = @{ - (__bridge id)kSecClass: (__bridge id)kSecClassCertificate, - (__bridge id)kSecValueRef: (__bridge id)cert, - (__bridge id)kSecAttrLabel: certAlias - }; - OSStatus status = SecItemAdd((__bridge CFDictionaryRef)certAttributes, NULL); - if (status != errSecSuccess && status != errSecDuplicateItem) { - RCTLogWarn(@"createIdentity: Failed to store certificate, status: %d", (int)status); - CFRelease(cert); - CFRelease(privateKey); - return NULL; - } - - - //------ PRIVATE API: need to find the proper way of doing it ------- - if (NO) { + if (YES) { identity = SecIdentityCreate(NULL, cert, privateKey); } else { + NSDictionary *findKeyAttributes = @{ + (__bridge id)kSecClass : (__bridge id)kSecClassKey, + (__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeRSA, + (__bridge id)kSecAttrKeyClass : (__bridge id)kSecAttrKeyClassPrivate, + (__bridge id)kSecReturnAttributes : @YES, + }; + status = SecItemCopyMatching((__bridge CFDictionaryRef)findKeyAttributes, (CFTypeRef *)&attributesRef); + if (status == errSecSuccess && attributesRef) { + attributes = (__bridge_transfer NSDictionary *)attributesRef; + NSLog(@"Key Attributes: %@", attributes); + } + NSDictionary *identityQuery = @{ - (__bridge id)kSecClass: (__bridge id)kSecClassIdentity, - (__bridge id)kSecReturnRef: @YES, - //(__bridge id)kSecMatchItemList:@[(__bridge id)cert], - (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne, - (__bridge id)kSecAttrLabel: certAlias, - //(__bridge id)kSecUseDataProtectionKeychain: @YES + (__bridge id)kSecClass : (__bridge id)kSecClassIdentity, + (__bridge id)kSecReturnRef : @YES, }; - status = SecItemCopyMatching((__bridge CFDictionaryRef)identityQuery, (CFTypeRef *)&identity); - + status = SecItemCopyMatching((__bridge CFDictionaryRef)identityQuery, + (CFTypeRef *)&identity); + if (status != errSecSuccess || !identity) { - RCTLogWarn(@"createIdentity: Failed to find identity, status: %d", (int)status); + RCTLogWarn(@"createIdentity: Failed to find identity, status: %d", + (int)status); } else { RCTLogWarn(@"createIdentity: Successfully found identity"); } } - + // Clean up CFRelease(cert); CFRelease(privateKey); - + return identity; } - (NSString *)stripPEMHeader:(NSString *)pemData prefix:(NSString *)prefix { NSMutableString *cleaned = [pemData mutableCopy]; - + // Remove header - NSString *header = [NSString stringWithFormat:@"-----BEGIN %@-----", prefix]; + NSString *header = + [NSString stringWithFormat:@"-----BEGIN %@-----", prefix]; [cleaned replaceOccurrencesOfString:header - withString:@"" - options:NSLiteralSearch - range:NSMakeRange(0, cleaned.length)]; - + withString:@"" + options:NSLiteralSearch + range:NSMakeRange(0, cleaned.length)]; + // Remove footer NSString *footer = [NSString stringWithFormat:@"-----END %@-----", prefix]; [cleaned replaceOccurrencesOfString:footer - withString:@"" - options:NSLiteralSearch - range:NSMakeRange(0, cleaned.length)]; - + withString:@"" + options:NSLiteralSearch + range:NSMakeRange(0, cleaned.length)]; + // Remove whitespace and newlines - NSArray *components = [cleaned componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + NSArray *components = + [cleaned componentsSeparatedByCharactersInSet: + [NSCharacterSet whitespaceAndNewlineCharacterSet]]; NSString *result = [components componentsJoinedByString:@""]; - + return result; } - (NSDictionary *)getPeerCertificate { + NSDictionary *result = nil; + if (!_tcpSocket || !_tls || !_peerTrust) { return nil; } - + SecCertificateRef certificate = SecTrustGetCertificateAtIndex(_peerTrust, 0); if (!certificate) { return nil; } - - return [self certificateToDict:certificate detailed:YES]; + + result = [self certificateToDict:certificate detailed:YES]; + return result; } - (NSDictionary *)getCertificate { + NSDictionary *result = nil; + if (!_tcpSocket || !_tls || !_resolvableCert) { return nil; } - + NSString *pemCert = [_resolvableCert resolve]; if (!pemCert) { return nil; } - - NSString *cleanedCert = [self stripPEMHeader:pemCert prefix:@"CERTIFICATE"]; - NSData *certData = [[NSData alloc] initWithBase64EncodedString:cleanedCert - options:NSDataBase64DecodingIgnoreUnknownCharacters]; - + + NSError *pemError = nil; + NSData *certData = [self getDataFromPEM:pemCert error:&pemError]; SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData); if (!certificate) { return nil; } - - NSDictionary *result = [self certificateToDict:certificate detailed:YES]; + + result = [self certificateToDict:certificate detailed:YES]; CFRelease(certificate); - + return result; } -// We need an ASN1 decoder to parse properly but for my case I only need modulus and exponent -// In addition SecCertificateCopyNormalizedIssuerSequence has some issues since it normalizes -// issuer and we don't want that -- (NSDictionary *)certificateToDict:(SecCertificateRef)certificate detailed:(BOOL)detailed { +// We need an ASN1 decoder to parse properly but for my case I only need modulus +// and exponent In addition SecCertificateCopyNormalizedIssuerSequence has some +// issues since it normalizes issuer and we don't want that +- (NSDictionary *)certificateToDict:(SecCertificateRef)certificate + detailed:(BOOL)detailed { NSMutableDictionary *certInfo = [NSMutableDictionary dictionary]; - + // Get public key info SecKeyRef publicKey = SecCertificateCopyKey(certificate); if (publicKey) { CFDictionaryRef attributes = SecKeyCopyAttributes(publicKey); if (attributes) { // Get key size (bits) - NSNumber *keySize = CFDictionaryGetValue(attributes, kSecAttrKeySizeInBits); + NSNumber *keySize = + CFDictionaryGetValue(attributes, kSecAttrKeySizeInBits); certInfo[@"bits"] = keySize; - + // Get modulus and exponent for RSA keys - CFDataRef keyData = SecKeyCopyExternalRepresentation(publicKey, NULL); + CFDataRef keyData = + SecKeyCopyExternalRepresentation(publicKey, NULL); if (keyData) { NSData *keyDataNS = (__bridge NSData *)keyData; - - // For RSA, the external representation is a DER-encoded RSAPublicKey + + // For RSA, the external representation is a DER-encoded + // RSAPublicKey if ([self isRSAKey:publicKey]) { NSArray *components = [self parseRSAPublicKey:keyDataNS]; if (components.count == 2) { @@ -814,7 +925,7 @@ - (NSDictionary *)certificateToDict:(SecCertificateRef)certificate detailed:(BOO certInfo[@"exponent"] = [NSString stringWithFormat:@"0x%@", components[1]]; } } - + // Add base64 encoded public key certInfo[@"pubkey"] = [keyDataNS base64EncodedStringWithOptions:0]; CFRelease(keyData); @@ -823,21 +934,24 @@ - (NSDictionary *)certificateToDict:(SecCertificateRef)certificate detailed:(BOO } CFRelease(publicKey); } - + // Get subject CFStringRef subjectName = NULL; OSStatus status = SecCertificateCopyCommonName(certificate, &subjectName); if (status == errSecSuccess && subjectName) { - certInfo[@"subject"] = @{@"CN": (__bridge_transfer NSString *)subjectName}; + certInfo[@"subject"] = + @{@"CN" : (__bridge_transfer NSString *)subjectName}; } - + // Get issuer using the normalized sequence - CFDataRef issuerSequence = SecCertificateCopyNormalizedIssuerSequence(certificate); + CFDataRef issuerSequence = + SecCertificateCopyNormalizedIssuerSequence(certificate); if (issuerSequence) { CFStringRef issuerName = NULL; status = SecCertificateCopyCommonName(certificate, &issuerName); if (status == errSecSuccess && issuerName) { - certInfo[@"issuer"] = @{@"CN": (__bridge_transfer NSString *)issuerName}; + certInfo[@"issuer"] = + @{@"CN" : (__bridge_transfer NSString *)issuerName}; } CFRelease(issuerSequence); } @@ -847,12 +961,13 @@ - (NSDictionary *)certificateToDict:(SecCertificateRef)certificate detailed:(BOO - (BOOL)isRSAKey:(SecKeyRef)key { CFDictionaryRef attributes = SecKeyCopyAttributes(key); - if (!attributes) return NO; - + if (!attributes) + return NO; + CFStringRef keyType = CFDictionaryGetValue(attributes, kSecAttrKeyType); BOOL isRSA = keyType && CFEqual(keyType, kSecAttrKeyTypeRSA); CFRelease(attributes); - + return isRSA; } @@ -882,22 +997,34 @@ - (NSArray *)parseRSAPublicKey:(NSData *)keyData { } } + // Skip leading zero if present for modulus + NSInteger startOffset = 0; + if (bytes[idx] == 0x00) { + startOffset = 1; + modulusLength--; + } + NSMutableString *modulus = [NSMutableString string]; for (NSInteger i = 0; i < modulusLength; i++) { - [modulus appendFormat:@"%02X", bytes[idx + i]]; + [modulus appendFormat:@"%02X", bytes[idx + startOffset + i]]; } - idx += modulusLength; + idx += modulusLength + startOffset; // Read exponent if (idx >= length || bytes[idx] != 0x02) return nil; idx++; NSInteger exponentLength = bytes[idx++]; - NSMutableString *exponent = [NSMutableString string]; + // Build exponent hex string + NSMutableString *exponentHex = [NSMutableString string]; for (NSInteger i = 0; i < exponentLength; i++) { - [exponent appendFormat:@"%02X", bytes[idx + i]]; + [exponentHex appendFormat:@"%02X", bytes[idx + i]]; } + // Remove leading zeros from the exponent + NSRegularExpression *re = [NSRegularExpression regularExpressionWithPattern:@"^0+(?=[0-9A-F]+)" options:0 error:nil]; + NSString *exponent = [re stringByReplacingMatchesInString:exponentHex options:0 range:NSMakeRange(0, exponentHex.length) withTemplate:@""]; + return @[modulus, exponent]; } @@ -915,5 +1042,4 @@ - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); } - @end From e2e5907dd1c295620a9bdf1c7a848b3076b852d5 Mon Sep 17 00:00:00 2001 From: Vince Ricosti <80467769+vricosti@users.noreply.github.com> Date: Thu, 6 Feb 2025 17:42:09 +0100 Subject: [PATCH 07/11] Clean and improvments --- ios/TcpSocketClient.h | 2 + ios/TcpSocketClient.m | 288 +++++++++++++++++++++--------------------- ios/TcpSockets.m | 8 ++ src/TLSSocket.js | 14 ++ 4 files changed, 171 insertions(+), 141 deletions(-) diff --git a/ios/TcpSocketClient.h b/ios/TcpSocketClient.h index bb82765..aa74c26 100644 --- a/ios/TcpSocketClient.h +++ b/ios/TcpSocketClient.h @@ -127,6 +127,8 @@ typedef enum RCTTCPError RCTTCPError; - (void)resume; ++ (BOOL)hasIdentity:(NSDictionary *)aliases; + /** * Get peer certificate information * @return NSDictionary with certificate information or nil if not available diff --git a/ios/TcpSocketClient.m b/ios/TcpSocketClient.m index 0853583..687379a 100644 --- a/ios/TcpSocketClient.m +++ b/ios/TcpSocketClient.m @@ -57,8 +57,6 @@ @interface TcpSocketClient () { BOOL _paused; BOOL _connecting; ResolvableOption *_resolvableCaCert; - ResolvableOption *_resolvableKey; - ResolvableOption *_resolvableCert; NSString *_host; GCDAsyncSocket *_tcpSocket; NSMutableDictionary *_pendingSends; @@ -67,6 +65,7 @@ @interface TcpSocketClient () { NSNumber *_serverId; long _sendTag; SecTrustRef _peerTrust; + SecIdentityRef _clientIdentity; } - (id)initWithClientId:(NSNumber *)clientID @@ -123,6 +122,17 @@ - (id)initWithClientId:(NSNumber *)clientID return self; } +- (void)dealloc { + if (_clientIdentity) { + CFRelease(_clientIdentity); + _clientIdentity = NULL; + } + if (_peerTrust) { + CFRelease(_peerTrust); + _peerTrust = NULL; + } +} + - (BOOL)connect:(NSString *)host port:(int)port withOptions:(NSDictionary *)options @@ -171,91 +181,91 @@ - (BOOL)connect:(NSString *)host - (ResolvableOption *)getResolvableOption:(NSDictionary *)tlsOptions forKey:(NSString *)key { - if (![tlsOptions objectForKey:key]) { + id value = [tlsOptions objectForKey:key]; + if (!value || ![value isKindOfClass:[NSString class]] || [(NSString *)value length] == 0) { return nil; } - NSString *value = tlsOptions[key]; NSArray *resolvedKeys = tlsOptions[@"resolvedKeys"]; - BOOL needsResolution = - resolvedKeys != nil && [resolvedKeys containsObject:key]; + BOOL needsResolution = resolvedKeys != nil && [resolvedKeys containsObject:key]; - return [ResolvableOption optionWithValue:value + return [ResolvableOption optionWithValue:(NSString *)value needsResolution:needsResolution]; } - (void)startTLS:(NSDictionary *)tlsOptions { if (_tls) return; - _tlsSettings = [NSMutableDictionary dictionary]; + NSMutableDictionary *settings = [NSMutableDictionary dictionary]; _resolvableCaCert = [self getResolvableOption:tlsOptions forKey:@"ca"]; - _resolvableKey = [self getResolvableOption:tlsOptions forKey:@"key"]; - _resolvableCert = [self getResolvableOption:tlsOptions forKey:@"cert"]; - NSString *keyAlias = tlsOptions[@"keyAlias"] ?: @"key"; - [(NSMutableDictionary *)_tlsSettings setObject:keyAlias forKey:@"keyAlias"]; - NSString *certAlias = tlsOptions[@"certAlias"] ?: @"cert"; - [(NSMutableDictionary *)_tlsSettings setObject:certAlias forKey:@"certAlias"]; - - RCTLogWarn( - @"startTLS: Resolved options - CA: %@, Key exists: %@, Cert exists: %@", - _resolvableCaCert ? @"YES" : @"NO", _resolvableKey ? @"YES" : @"NO", - _resolvableCert ? @"YES" : @"NO"); - BOOL checkValidity = (tlsOptions[@"rejectUnauthorized"] ? [tlsOptions[@"rejectUnauthorized"] boolValue] : true); if (!checkValidity) { // Do not validate - RCTLogWarn(@"startTLS: Validation disabled"); _checkValidity = false; - [(NSMutableDictionary *)_tlsSettings setObject:[NSNumber numberWithBool:YES] + [settings setObject:[NSNumber numberWithBool:YES] forKey:GCDAsyncSocketManuallyEvaluateTrust]; } else if (_resolvableCaCert != nil) { // Self-signed certificate - RCTLogWarn(@"startTLS: Using self-signed certificate validation"); - [(NSMutableDictionary *)_tlsSettings setObject:[NSNumber numberWithBool:YES] + [settings setObject:[NSNumber numberWithBool:YES] forKey:GCDAsyncSocketManuallyEvaluateTrust]; } else { // Default certificates - RCTLogWarn( - @"startTLS: Using default certificate validation with host: %@", - _host); - [(NSMutableDictionary *)_tlsSettings setObject:_host forKey:(NSString *)kCFStreamSSLPeerName]; + [settings setObject:_host forKey:(NSString *)kCFStreamSSLPeerName]; } - + /////////////////////////////////////////////////////////////////////////////////////////////////////////////: // Handle client certificate authentication - if (_resolvableCert != nil && _resolvableKey != nil) { - RCTLogWarn(@"startTLS: Attempting client certificate authentication"); - NSString *pemCert = [_resolvableCert resolve]; - NSString *pemKey = [_resolvableKey resolve]; + /////////////////////////////////////////////////////////////////////////////////////////////////////////////: + SecIdentityRef myIdent = NULL; + ResolvableOption *resolvableKey = [self getResolvableOption:tlsOptions forKey:@"key"]; + ResolvableOption *resolvableCert = [self getResolvableOption:tlsOptions forKey:@"cert"]; + NSString *keyAlias = tlsOptions[@"keyAlias"]; + if (keyAlias) { [settings setObject:keyAlias forKey:@"keyAlias"]; } + NSString *certAlias = tlsOptions[@"certAlias"]; + if (certAlias) { [settings setObject:certAlias forKey:@"certAlias"]; } + + // if user provides certAlias without cert it means an identity(cert+key) has already been + // inserted in keychain. + if ((certAlias && certAlias.length > 0) && (!resolvableCert)) { + //RCTLogWarn(@"startTLS: Trying to find existing identity with certAlias %@", certAlias); + NSDictionary *identityQuery = @{ + (__bridge id)kSecClass : (__bridge id)kSecClassIdentity, + (__bridge id)kSecReturnRef : @YES, + (__bridge id)kSecAttrLabel : certAlias + }; + SecItemCopyMatching((__bridge CFDictionaryRef)identityQuery, (CFTypeRef *)&myIdent); + + } else if (resolvableCert != nil && resolvableKey != nil) { + //RCTLogWarn(@"startTLS: Attempting client certificate authentication"); + NSString *pemCert = [resolvableCert resolve]; + NSString *pemKey = [resolvableKey resolve]; - RCTLogWarn( - @"startTLS: Resolved PEM cert exists: %@, PEM key exists: %@", - pemCert ? @"YES" : @"NO", pemKey ? @"YES" : @"NO"); +// RCTLogWarn( +// @"startTLS: Resolved PEM cert exists: %@, PEM key exists: %@", +// pemCert ? @"YES" : @"NO", pemKey ? @"YES" : @"NO"); if (pemCert && pemKey) { - SecIdentityRef identity = [self createIdentityWithCert:pemCert - privateKey:pemKey - settings:_tlsSettings]; - RCTLogWarn(@"startTLS: Identity creation %@", - identity ? @"successful" : @"failed"); - if (identity) { - NSArray *certificates = @[ (__bridge id)identity ]; - [(NSMutableDictionary *)_tlsSettings setObject:certificates - forKey:(NSString *)kCFStreamSSLCertificates]; - CFRelease(identity); - RCTLogWarn( - @"startTLS: Client certificates configured successfully"); - } else { - RCTLogWarn( - @"startTLS: Failed to create identity from cert and key"); - } + myIdent = [self createIdentityWithCert:pemCert + privateKey:pemKey + settings:settings]; + //RCTLogWarn(@"startTLS: Identity creation %@", myIdent ? @"successful" : @"failed"); } } + + if (myIdent) { + if (_clientIdentity) { CFRelease(_clientIdentity); } + _clientIdentity = (SecIdentityRef)CFRetain(myIdent); + + NSArray *myCerts = @[ (__bridge id)myIdent ]; + [settings setObject:myCerts + forKey:(NSString *)kCFStreamSSLCertificates]; + //RCTLogWarn(@"startTLS: Client certificates configured successfully"); + } - RCTLogWarn(@"startTLS: Final settings: %@", _tlsSettings); + //RCTLogWarn(@"startTLS: Final settings: %@", settings); _tls = true; - [_tcpSocket startTLS:_tlsSettings]; + [_tcpSocket startTLS:settings]; } - (NSDictionary *)getAddress { @@ -602,12 +612,6 @@ - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err { withError:(!err || err.code == GCDAsyncSocketClosedError ? nil : err)]; } -// https://stackoverflow.com/questions/45997841/how-to-get-a-secidentityref-from-a-seccertificateref-and-a-seckeyref -// We use a private API call here. -SecIdentityRef SecIdentityCreate(CFAllocatorRef allocator, - SecCertificateRef certificate, - SecKeyRef privateKey); - typedef NS_ENUM(NSInteger, PEMType) { PEMTypeUnknown, PEMTypeCertificate, @@ -665,20 +669,48 @@ - (NSData *)getDataFromPEM:(NSString *)pemData error:(NSError **)error { NSDataBase64DecodingIgnoreUnknownCharacters]; // For PKCS#8, extract the RSA key if (type == PEMTypePKCS8) { - return [self extractRSAKeyFromPKCS8:decodedData]; + return [self extractRSAKeyFromPKCS8:decodedData error:error]; } return decodedData; } -- (NSData *)extractRSAKeyFromPKCS8:(NSData *)pkcs8Data { - // PKCS#8 format has a 26-byte header before the RSA private key - // For a proper solution, you should use ASN.1 parsing - // This is a temporary solution that works with standard RSA keys - if (pkcs8Data.length <= 26) { +- (NSData *)extractRSAKeyFromPKCS8:(NSData *)pkcs8Data error:(NSError **)error { + // RSA 2048-bit PKCS#8 header (26 bytes) + const uint8_t rsa2048Prefix[] = { + 0x30, 0x82, 0x04, 0xBE, 0x02, 0x01, 0x00, 0x30, + 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, + 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00, 0x04, 0x82, + 0x04, 0xA8 + }; + + // Check for minimum data length + if (pkcs8Data.length <= sizeof(rsa2048Prefix)) { + if (error) { + *error = [NSError errorWithDomain:RCTTCPErrorDomain + code:-1 + userInfo:@{ + NSLocalizedDescriptionKey: @"Invalid PKCS#8 data: too short" + }]; + } + return nil; + } + + // Verify RSA 2048-bit key prefix - We need a ASN1 decoder to support more !!! + if (memcmp(pkcs8Data.bytes, rsa2048Prefix, sizeof(rsa2048Prefix)) != 0) { + if (error) { + *error = [NSError errorWithDomain:RCTTCPErrorDomain + code:-1 + userInfo:@{ + NSLocalizedDescriptionKey: @"Unsupported key format: only RSA 2048-bit keys are supported" + }]; + } return nil; } - return [pkcs8Data subdataWithRange:NSMakeRange(26, pkcs8Data.length - 26)]; + + // Extract the RSA private key data + return [pkcs8Data subdataWithRange:NSMakeRange(sizeof(rsa2048Prefix), + pkcs8Data.length - sizeof(rsa2048Prefix))]; } - (SecIdentityRef)createIdentityWithCert:(NSString *)pemCert @@ -688,11 +720,10 @@ - (SecIdentityRef)createIdentityWithCert:(NSString *)pemCert OSStatus status = -1; SecIdentityRef identity = NULL; - NSData *publicKeyHash = nil; // Get aliases from settings - NSString *certAlias = settings[@"certAlias"]; - NSString *keyAlias = settings[@"keyAlias"]; + NSString *certAlias = settings[@"certAlias"] ?: @"clientTlsCert"; + NSString *keyAlias = settings[@"keyAlias"] ?: @"clientTlsKey"; NSError *pemError = nil; NSData *certData = [self getDataFromPEM:pemCert error:&pemError]; @@ -712,12 +743,12 @@ - (SecIdentityRef)createIdentityWithCert:(NSString *)pemCert // Import certificate in keychain NSDictionary *deleteCertQuery = @{ (__bridge id)kSecClass : (__bridge id)kSecClassCertificate, - //(__bridge id)kSecAttrLabel: certAlias + (__bridge id)kSecAttrLabel: certAlias }; status = SecItemDelete((__bridge CFDictionaryRef)deleteCertQuery); NSDictionary *certAttributes = @{ - (__bridge id)kSecClass : (__bridge id)kSecClassCertificate, + //(__bridge id)kSecClass : (__bridge id)kSecClassCertificate, (__bridge id)kSecValueRef : (__bridge id)cert, (__bridge id)kSecAttrLabel : certAlias, }; @@ -728,27 +759,11 @@ - (SecIdentityRef)createIdentityWithCert:(NSString *)pemCert CFRelease(cert); return NULL; } - // Retrieve pkhh and use it to set kSecAttrApplicationLabel when importing - // private key - CFDictionaryRef attributesRef = NULL; - NSDictionary *attributes = nil; - NSDictionary *findCertAttributes = @{ - (__bridge id)kSecClass : (__bridge id)kSecClassCertificate, - (__bridge id)kSecReturnAttributes : @YES - }; - status = SecItemCopyMatching((__bridge CFDictionaryRef)findCertAttributes, - (CFTypeRef *)&attributesRef); - if (status == errSecSuccess && attributesRef) { - attributes = (__bridge_transfer NSDictionary *)attributesRef; - NSLog(@"Cert Attributes: %@", attributes); - publicKeyHash = attributes[(__bridge id)kSecAttrPublicKeyHash]; - RCTLogWarn(@"Retrieved public key hash from certificate"); - } - + NSDictionary *privateKeyAttributes = @{ (__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeRSA, (__bridge id)kSecAttrKeyClass : (__bridge id)kSecAttrKeyClassPrivate, - (__bridge id)kSecReturnPersistentRef : @YES + //(__bridge id)kSecReturnPersistentRef : @YES }; CFErrorRef error = NULL; SecKeyRef privateKey = SecKeyCreateWithData( @@ -764,20 +779,16 @@ - (SecIdentityRef)createIdentityWithCert:(NSString *)pemCert NSDictionary *deleteKeyQuery = @{ (__bridge id)kSecClass : (__bridge id)kSecClassKey, - (__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeRSA, + //(__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeRSA, + (__bridge id)kSecAttrLabel: keyAlias }; status = SecItemDelete((__bridge CFDictionaryRef)deleteKeyQuery); // Add the private key to keychain NSDictionary *keyAttributes = @{ - (__bridge id)kSecClass : (__bridge id)kSecClassKey, - (__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeRSA, - (__bridge id)kSecAttrKeyClass : (__bridge id)kSecAttrKeyClassPrivate, - (__bridge id)kSecAttrIsPermanent : @"YES", - (__bridge id)kSecAttrLabel : keyAlias, - (__bridge id)kSecAttrApplicationLabel : publicKeyHash + (__bridge id)kSecValueRef: (__bridge id)privateKey, + (__bridge id)kSecAttrLabel : keyAlias }; - status = SecItemAdd((__bridge CFDictionaryRef)keyAttributes, NULL); if (status != errSecSuccess && status != errSecDuplicateItem) { RCTLogWarn(@"createIdentity: Failed to store private key, status: %d", @@ -786,42 +797,26 @@ - (SecIdentityRef)createIdentityWithCert:(NSString *)pemCert CFRelease(privateKey); return NULL; } - - //------ PRIVATE API: need to find the proper way of doing it ------- - if (YES) { - identity = SecIdentityCreate(NULL, cert, privateKey); + + NSDictionary *identityQuery = @{ + (__bridge id)kSecClass : (__bridge id)kSecClassIdentity, + (__bridge id)kSecReturnRef : @YES, + (__bridge id)kSecAttrLabel : certAlias + }; + status = SecItemCopyMatching((__bridge CFDictionaryRef)identityQuery, + (CFTypeRef *)&identity); + + if (status != errSecSuccess || !identity) { + RCTLogWarn(@"createIdentity: Failed to find identity, status: %d", + (int)status); } else { - NSDictionary *findKeyAttributes = @{ - (__bridge id)kSecClass : (__bridge id)kSecClassKey, - (__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeRSA, - (__bridge id)kSecAttrKeyClass : (__bridge id)kSecAttrKeyClassPrivate, - (__bridge id)kSecReturnAttributes : @YES, - }; - status = SecItemCopyMatching((__bridge CFDictionaryRef)findKeyAttributes, (CFTypeRef *)&attributesRef); - if (status == errSecSuccess && attributesRef) { - attributes = (__bridge_transfer NSDictionary *)attributesRef; - NSLog(@"Key Attributes: %@", attributes); - } - - NSDictionary *identityQuery = @{ - (__bridge id)kSecClass : (__bridge id)kSecClassIdentity, - (__bridge id)kSecReturnRef : @YES, - }; - status = SecItemCopyMatching((__bridge CFDictionaryRef)identityQuery, - (CFTypeRef *)&identity); - - if (status != errSecSuccess || !identity) { - RCTLogWarn(@"createIdentity: Failed to find identity, status: %d", - (int)status); - } else { - RCTLogWarn(@"createIdentity: Successfully found identity"); - } + RCTLogWarn(@"createIdentity: Successfully found identity"); } - + // Clean up CFRelease(cert); CFRelease(privateKey); - + return identity; } @@ -852,6 +847,19 @@ - (NSString *)stripPEMHeader:(NSString *)pemData prefix:(NSString *)prefix { return result; } ++ (BOOL)hasIdentity:(NSDictionary *)aliases { + NSString *certAlias = aliases[@"certAlias"]; + if (!certAlias) { + return NO; + } + NSDictionary *identityQuery = @{ + (__bridge id)kSecClass : (__bridge id)kSecClassIdentity, + (__bridge id)kSecAttrLabel : certAlias + }; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)identityQuery, (CFTypeRef *)NULL); + return status == errSecSuccess; +} + - (NSDictionary *)getPeerCertificate { NSDictionary *result = nil; @@ -871,20 +879,18 @@ - (NSDictionary *)getPeerCertificate { - (NSDictionary *)getCertificate { NSDictionary *result = nil; - if (!_tcpSocket || !_tls || !_resolvableCert) { + if (!_tcpSocket || !_tls) { return nil; } - - NSString *pemCert = [_resolvableCert resolve]; - if (!pemCert) { - return nil; - } - - NSError *pemError = nil; - NSData *certData = [self getDataFromPEM:pemCert error:&pemError]; - SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData); - if (!certificate) { - return nil; + + SecCertificateRef certificate = NULL; + if (_clientIdentity) { + // If we have a client identity, get the certificate from it + OSStatus status = SecIdentityCopyCertificate(_clientIdentity, &certificate); + if (status != errSecSuccess) { + RCTLogWarn(@"getCertificate: Failed to get certificate from identity, status: %d", (int)status); + return nil; + } } result = [self certificateToDict:certificate detailed:YES]; diff --git a/ios/TcpSockets.m b/ios/TcpSockets.m index 3aca377..fc99b73 100644 --- a/ios/TcpSockets.m +++ b/ios/TcpSockets.m @@ -175,6 +175,14 @@ - (TcpSocketClient *)createSocket:(nonnull NSNumber *)cId { [client resume]; } +RCT_EXPORT_METHOD(hasIdentity:(nonnull NSDictionary *)aliases + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + // Since this is a static check of the keychain, we don't need a client instance + BOOL hasIdentity = [TcpSocketClient hasIdentity:aliases]; + resolve(@(hasIdentity)); +} + RCT_EXPORT_METHOD(getPeerCertificate:(nonnull NSNumber *)cId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { diff --git a/src/TLSSocket.js b/src/TLSSocket.js index d538638..36bee55 100644 --- a/src/TLSSocket.js +++ b/src/TLSSocket.js @@ -66,6 +66,20 @@ export default class TLSSocket extends Socket { Sockets.startTLS(this._id, this._options); } + /** + * Checks if a certificate identity exists in the keychain + * @param {object} options Object containing the identity aliases + * @param {string} [options.certAlias] The certificate alias + * @param {string} [options.keyAlias] The key alias + * @returns {Promise} Promise resolving to true if identity exists + */ + static hasIdentity(options = {}) { + return Sockets.hasIdentity({ + certAlias: options.certAlias, + keyAlias: options.keyAlias + }); + } + getCertificate() { return Sockets.getCertificate(this._id); } From d3401bb7d8fad2736127d3a0ca4701c4d94a14e7 Mon Sep 17 00:00:00 2001 From: Vince Ricosti <80467769+vricosti@users.noreply.github.com> Date: Fri, 7 Feb 2025 16:09:19 +0100 Subject: [PATCH 08/11] add hasIdentity to TLSSocket.js --- ios/TcpSocketClient.m | 11 ----------- lib/types/TLSSocket.d.ts | 13 +++++++++++++ lib/types/index.d.ts | 1 + src/TLSSocket.js | 4 +++- src/index.js | 2 ++ 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/ios/TcpSocketClient.m b/ios/TcpSocketClient.m index 687379a..e43eaf8 100644 --- a/ios/TcpSocketClient.m +++ b/ios/TcpSocketClient.m @@ -122,17 +122,6 @@ - (id)initWithClientId:(NSNumber *)clientID return self; } -- (void)dealloc { - if (_clientIdentity) { - CFRelease(_clientIdentity); - _clientIdentity = NULL; - } - if (_peerTrust) { - CFRelease(_peerTrust); - _peerTrust = NULL; - } -} - - (BOOL)connect:(NSString *)host port:(int)port withOptions:(NSDictionary *)options diff --git a/lib/types/TLSSocket.d.ts b/lib/types/TLSSocket.d.ts index bc1f2b7..c512abd 100644 --- a/lib/types/TLSSocket.d.ts +++ b/lib/types/TLSSocket.d.ts @@ -11,6 +11,19 @@ * @extends {Socket} */ export default class TLSSocket extends Socket { + /** + * Checks if a certificate identity exists in the keychain + * @param {object} options Object containing the identity aliases + * @param {string} [options.androidKeyStore] The android keystore type + * @param {string} [options.certAlias] The certificate alias + * @param {string} [options.keyAlias] The key alias + * @returns {Promise} Promise resolving to true if identity exists + */ + static hasIdentity(options?: { + androidKeyStore: string | undefined; + certAlias: string | undefined; + keyAlias: string | undefined; + }): Promise; /** * @private * Resolves the asset source if necessary and registers the resolved key. diff --git a/lib/types/index.d.ts b/lib/types/index.d.ts index 328f349..79ac618 100644 --- a/lib/types/index.d.ts +++ b/lib/types/index.d.ts @@ -11,6 +11,7 @@ declare namespace _default { export { Socket }; export { TLSServer }; export { TLSSocket }; + export const hasIdentity: typeof import("./TLSSocket").default.hasIdentity; } export default _default; /** diff --git a/src/TLSSocket.js b/src/TLSSocket.js index 36bee55..f308991 100644 --- a/src/TLSSocket.js +++ b/src/TLSSocket.js @@ -69,14 +69,16 @@ export default class TLSSocket extends Socket { /** * Checks if a certificate identity exists in the keychain * @param {object} options Object containing the identity aliases + * @param {string} [options.androidKeyStore] The android keystore type * @param {string} [options.certAlias] The certificate alias * @param {string} [options.keyAlias] The key alias * @returns {Promise} Promise resolving to true if identity exists */ static hasIdentity(options = {}) { return Sockets.hasIdentity({ + androidKeyStore: options.androidKeyStore, certAlias: options.certAlias, - keyAlias: options.keyAlias + keyAlias: options.keyAlias, }); } diff --git a/src/index.js b/src/index.js index e04e240..2237dbc 100644 --- a/src/index.js +++ b/src/index.js @@ -112,6 +112,7 @@ export default { Socket, TLSServer, TLSSocket, + hasIdentity: TLSSocket.hasIdentity, }; // @ts-ignore @@ -128,4 +129,5 @@ module.exports = { Socket, TLSServer, TLSSocket, + hasIdentity: TLSSocket.hasIdentity, }; From 7b548aea1c0de1d4b6c5328958d5103713f99c17 Mon Sep 17 00:00:00 2001 From: Vince Ricosti <80467769+vricosti@users.noreply.github.com> Date: Sat, 8 Feb 2025 17:18:52 +0100 Subject: [PATCH 09/11] also add hasIdentity to android to be consistent --- .../react/tcpsocket/SSLCertificateHelper.java | 24 +++++++++++++++++++ .../react/tcpsocket/TcpSocketModule.java | 10 ++++++++ 2 files changed, 34 insertions(+) diff --git a/android/src/main/java/com/asterinet/react/tcpsocket/SSLCertificateHelper.java b/android/src/main/java/com/asterinet/react/tcpsocket/SSLCertificateHelper.java index 9d830cb..a01de3a 100644 --- a/android/src/main/java/com/asterinet/react/tcpsocket/SSLCertificateHelper.java +++ b/android/src/main/java/com/asterinet/react/tcpsocket/SSLCertificateHelper.java @@ -80,6 +80,30 @@ static SSLServerSocketFactory createServerSocketFactory(Context context, @NonNul return sslContext.getServerSocketFactory(); } + static boolean hasIdentity(ReadableMap options) { + boolean hasId = false; + try { + final String keystoreName = options.hasKey("androidKeyStore") ? + options.getString("androidKeyStore") : KeyStore.getDefaultType(); + final String keyAlias = options.hasKey("keyAlias") ? + options.getString("keyAlias") : ""; + + if (keyAlias.isEmpty()) { + return false; + } + + // Get keystore instance + KeyStore keyStore = KeyStore.getInstance(keystoreName); + keyStore.load(null, null); + + // Check if key entry exists with its certificate chain + hasId = keyStore.isKeyEntry(keyAlias); + return hasId; + } catch (Exception e) { + return false; + } + } + public static PrivateKey getPrivateKeyFromPEM(InputStream keyStream) { try (PemReader pemReader = new PemReader(new InputStreamReader(keyStream))) { PemObject pemObject = pemReader.readPemObject(); diff --git a/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketModule.java b/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketModule.java index 326f7ef..c8f791f 100644 --- a/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketModule.java +++ b/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketModule.java @@ -384,6 +384,16 @@ private TcpSocketServer getTcpServer(final int id) { return (TcpSocketServer) socket; } + @SuppressWarnings("unused") + @ReactMethod + public void hasIdentity(@NonNull final ReadableMap options, Promise promise) { + try { + promise.resolve(SSLCertificateHelper.hasIdentity(options)); + } catch (Exception e) { + promise.reject(e); + } + } + @SuppressWarnings("unused") @ReactMethod public void getPeerCertificate(final int cId, Promise promise) { From 89eebc488c7d0451610af5d9e96ee39d25444394 Mon Sep 17 00:00:00 2001 From: Vince Ricosti <80467769+vricosti@users.noreply.github.com> Date: Sun, 9 Feb 2025 20:50:01 +0100 Subject: [PATCH 10/11] Incorrect implementation of .setTimeout - with fix #194 --- src/Socket.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Socket.js b/src/Socket.js index 4268015..6ed2141 100644 --- a/src/Socket.js +++ b/src/Socket.js @@ -182,10 +182,11 @@ export default class Socket extends EventEmitter { * @param {() => void} [callback] */ setTimeout(timeout, callback) { - if (timeout === 0) { + this._timeoutMsecs = timeout; + if (this._timeoutMsecs === 0) { this._clearTimeout(); } else { - this._activateTimer(timeout); + this._resetTimeout(); } if (callback) this.once('timeout', callback); return this; @@ -193,15 +194,15 @@ export default class Socket extends EventEmitter { /** * @private - * @param {number} [timeout] */ - _activateTimer(timeout) { - if (timeout !== undefined) this._timeoutMsecs = timeout; - this._clearTimeout(); - this._timeout = setTimeout(() => { + _resetTimeout() { + if (this._timeoutMsecs !== 0) { this._clearTimeout(); - this.emit('timeout'); - }, this._timeoutMsecs); + this._timeout = setTimeout(() => { + this._clearTimeout(); + this.emit('timeout'); + }, this._timeoutMsecs); + } } /** @@ -327,7 +328,6 @@ export default class Socket extends EventEmitter { * @return {boolean} */ write(buffer, encoding, cb) { - const self = this; if (this._pending || this._destroyed) throw new Error('Socket is closed.'); const generatedBuffer = this._generateSendBuffer(buffer, encoding); @@ -340,7 +340,7 @@ export default class Socket extends EventEmitter { this._msgEvtEmitter.removeListener('written', msgEvtHandler); this._writeBufferSize -= generatedBuffer.byteLength; this._lastRcvMsgId = msgId; - if (self._timeout) self._activateTimer(); + this._resetTimeout(); if (this.writableNeedDrain && this._lastSentMsgId === msgId) { this.writableNeedDrain = false; this.emit('drain'); @@ -434,6 +434,7 @@ export default class Socket extends EventEmitter { */ _onDeviceDataEvt = (/** @type {{ id: number; data: string; }} */ evt) => { if (evt.id !== this._id) return; + this._resetTimeout(); if (!this._paused) { const bufferData = Buffer.from(evt.data, 'base64'); this._bytesRead += bufferData.byteLength; From f1ab919b18dbd11b72e112b69e37f60cffcf5600 Mon Sep 17 00:00:00 2001 From: Vince Ricosti <80467769+vricosti@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:42:43 +0100 Subject: [PATCH 11/11] final clean and implement same behavior on android --- .../react/tcpsocket/SSLCertificateHelper.java | 54 ++++++++++--------- .../react/tcpsocket/TcpSocketClient.java | 7 ++- ios/TcpSocketClient.m | 19 ++----- lib/types/Socket.d.ts | 3 +- 4 files changed, 41 insertions(+), 42 deletions(-) diff --git a/android/src/main/java/com/asterinet/react/tcpsocket/SSLCertificateHelper.java b/android/src/main/java/com/asterinet/react/tcpsocket/SSLCertificateHelper.java index a01de3a..1e0622f 100644 --- a/android/src/main/java/com/asterinet/react/tcpsocket/SSLCertificateHelper.java +++ b/android/src/main/java/com/asterinet/react/tcpsocket/SSLCertificateHelper.java @@ -81,7 +81,6 @@ static SSLServerSocketFactory createServerSocketFactory(Context context, @NonNul } static boolean hasIdentity(ReadableMap options) { - boolean hasId = false; try { final String keystoreName = options.hasKey("androidKeyStore") ? options.getString("androidKeyStore") : KeyStore.getDefaultType(); @@ -92,13 +91,11 @@ static boolean hasIdentity(ReadableMap options) { return false; } - // Get keystore instance KeyStore keyStore = KeyStore.getInstance(keystoreName); keyStore.load(null, null); // Check if key entry exists with its certificate chain - hasId = keyStore.isKeyEntry(keyAlias); - return hasId; + return keyStore.isKeyEntry(keyAlias); } catch (Exception e) { return false; } @@ -151,29 +148,35 @@ static SSLSocketFactory createCustomTrustedSocketFactory( final KeystoreInfo keystoreInfo) throws IOException, GeneralSecurityException { SSLSocketFactory ssf = null; - if (optionResCert != null && optionResKey != null) { - final String keyStoreName = keystoreInfo.getKeystoreName().isEmpty() ? + + KeyStore keyStore = null; + final String keyStoreName = keystoreInfo.getKeystoreName().isEmpty() ? KeyStore.getDefaultType() : keystoreInfo.getKeystoreName(); - KeyStore keyStore = KeyStore.getInstance(keyStoreName); - keyStore.load(null, null); + String keyAlias = keystoreInfo.getKeyAlias(); - // Check if cert and key if already registered inside our keystore - // If one is missing we insert again - boolean hasCertInStore = keyStore.isCertificateEntry(keystoreInfo.getCertAlias()); - boolean hasKeyInStore = keyStore.isKeyEntry(keystoreInfo.getKeyAlias()); - if (!hasCertInStore || !hasKeyInStore) { - InputStream certInput = getResolvableinputStream(context, optionResCert); - Certificate cert = CertificateFactory.getInstance("X.509").generateCertificate(certInput); - keyStore.setCertificateEntry(keystoreInfo.getCertAlias(), cert); - - InputStream keyInput = getResolvableinputStream(context, optionResKey); - PrivateKey privateKey = getPrivateKeyFromPEM(keyInput); - keyStore.setKeyEntry(keystoreInfo.getKeyAlias(), privateKey, null, new Certificate[]{cert}); + // if user provides keyAlias without key it means an identity(cert+key) has already been + // inserted in keychain. + if (keyAlias != null && !keyAlias.isEmpty() && optionResKey == null) { + keyStore = KeyStore.getInstance(keyStoreName); + keyStore.load(null, null); + if (!keyStore.isKeyEntry(keyAlias)) { + keyStore = null; } + } else if (optionResCert != null && optionResKey != null) { + + keyStore = KeyStore.getInstance(keyStoreName); + keyStore.load(null, null); - boolean hasCaInStore = keyStore.isCertificateEntry(keystoreInfo.getCaAlias()); - if (optionResCa != null && !hasCaInStore) { + InputStream certInput = getResolvableinputStream(context, optionResCert); + Certificate cert = CertificateFactory.getInstance("X.509").generateCertificate(certInput); + keyStore.setCertificateEntry(keystoreInfo.getCertAlias(), cert); + + InputStream keyInput = getResolvableinputStream(context, optionResKey); + PrivateKey privateKey = getPrivateKeyFromPEM(keyInput); + keyStore.setKeyEntry(keystoreInfo.getKeyAlias(), privateKey, null, new Certificate[]{cert}); + + if (optionResCa != null) { InputStream caInput = getResolvableinputStream(context, optionResCa); // Generate the CA Certificate from the raw resource file Certificate ca = CertificateFactory.getInstance("X.509").generateCertificate(caInput); @@ -181,7 +184,9 @@ static SSLSocketFactory createCustomTrustedSocketFactory( // Load the key store using the CA keyStore.setCertificateEntry(keystoreInfo.getCaAlias(), ca); } - + } + + if (keyStore != null) { // Initialize the KeyManagerFactory with this cert KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(keyStore, new char[0]); @@ -190,7 +195,6 @@ static SSLSocketFactory createCustomTrustedSocketFactory( SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyManagerFactory.getKeyManagers(), new TrustManager[]{new BlindTrustManager()}, null); return sslContext.getSocketFactory(); - } else { // Keep old behavior InputStream caInput = getResolvableinputStream(context, optionResCa); @@ -198,7 +202,7 @@ static SSLSocketFactory createCustomTrustedSocketFactory( Certificate ca = CertificateFactory.getInstance("X.509").generateCertificate(caInput); caInput.close(); // Load the key store using the CA - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null, null); keyStore.setCertificateEntry("ca", ca); diff --git a/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketClient.java b/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketClient.java index 6e745a4..6ef8269 100644 --- a/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketClient.java +++ b/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketClient.java @@ -87,9 +87,13 @@ private boolean containsKey(ReadableArray array, String key) { } return false; } + private ResolvableOption getResolvableOption(ReadableMap tlsOptions, String key) { if (tlsOptions.hasKey(key)) { String value = tlsOptions.getString(key); + if (value == null || value.isEmpty()) { + return null; + } ReadableArray resolvedKeys = tlsOptions.hasKey("resolvedKeys") ? tlsOptions.getArray("resolvedKeys") : null; boolean needsResolution = resolvedKeys != null && containsKey(resolvedKeys, key); return new ResolvableOption(value, needsResolution); @@ -110,7 +114,8 @@ private SSLSocketFactory getSSLSocketFactory(Context context, ReadableMap tlsOpt final KeystoreInfo keystoreInfo = new KeystoreInfo(keystoreName, caAlias, certAlias, keyAlias); if (tlsOptions.hasKey("rejectUnauthorized") && !tlsOptions.getBoolean("rejectUnauthorized")) { - if (customTlsKey != null && customTlsCert != null ) { + if ((customTlsKey != null && customTlsCert != null) || + (keyAlias != null && !keyAlias.isEmpty() && customTlsKey == null) ) { ssf = SSLCertificateHelper.createCustomTrustedSocketFactory( context, customTlsCa, diff --git a/ios/TcpSocketClient.m b/ios/TcpSocketClient.m index e43eaf8..ad52d6c 100644 --- a/ios/TcpSocketClient.m +++ b/ios/TcpSocketClient.m @@ -229,11 +229,6 @@ - (void)startTLS:(NSDictionary *)tlsOptions { //RCTLogWarn(@"startTLS: Attempting client certificate authentication"); NSString *pemCert = [resolvableCert resolve]; NSString *pemKey = [resolvableKey resolve]; - -// RCTLogWarn( -// @"startTLS: Resolved PEM cert exists: %@, PEM key exists: %@", -// pemCert ? @"YES" : @"NO", pemKey ? @"YES" : @"NO"); - if (pemCert && pemKey) { myIdent = [self createIdentityWithCert:pemCert privateKey:pemKey @@ -705,8 +700,6 @@ - (NSData *)extractRSAKeyFromPKCS8:(NSData *)pkcs8Data error:(NSError **)error { - (SecIdentityRef)createIdentityWithCert:(NSString *)pemCert privateKey:(NSString *)pemKey settings:(NSDictionary *)settings { - RCTLogWarn(@"createIdentity: Starting identity creation"); - OSStatus status = -1; SecIdentityRef identity = NULL; @@ -732,7 +725,8 @@ - (SecIdentityRef)createIdentityWithCert:(NSString *)pemCert // Import certificate in keychain NSDictionary *deleteCertQuery = @{ (__bridge id)kSecClass : (__bridge id)kSecClassCertificate, - (__bridge id)kSecAttrLabel: certAlias + (__bridge id)kSecAttrLabel: certAlias, + (__bridge id)kSecReturnRef : @YES }; status = SecItemDelete((__bridge CFDictionaryRef)deleteCertQuery); @@ -751,8 +745,7 @@ - (SecIdentityRef)createIdentityWithCert:(NSString *)pemCert NSDictionary *privateKeyAttributes = @{ (__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeRSA, - (__bridge id)kSecAttrKeyClass : (__bridge id)kSecAttrKeyClassPrivate, - //(__bridge id)kSecReturnPersistentRef : @YES + (__bridge id)kSecAttrKeyClass : (__bridge id)kSecAttrKeyClassPrivate }; CFErrorRef error = NULL; SecKeyRef privateKey = SecKeyCreateWithData( @@ -768,8 +761,8 @@ - (SecIdentityRef)createIdentityWithCert:(NSString *)pemCert NSDictionary *deleteKeyQuery = @{ (__bridge id)kSecClass : (__bridge id)kSecClassKey, - //(__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeRSA, - (__bridge id)kSecAttrLabel: keyAlias + (__bridge id)kSecAttrLabel: keyAlias, + (__bridge id)kSecReturnRef : @YES }; status = SecItemDelete((__bridge CFDictionaryRef)deleteKeyQuery); @@ -798,8 +791,6 @@ - (SecIdentityRef)createIdentityWithCert:(NSString *)pemCert if (status != errSecSuccess || !identity) { RCTLogWarn(@"createIdentity: Failed to find identity, status: %d", (int)status); - } else { - RCTLogWarn(@"createIdentity: Successfully found identity"); } // Clean up diff --git a/lib/types/Socket.d.ts b/lib/types/Socket.d.ts index fc793b2..92cbd46 100644 --- a/lib/types/Socket.d.ts +++ b/lib/types/Socket.d.ts @@ -119,9 +119,8 @@ export default class Socket extends EventEmitter void) | undefined): Socket; /** * @private - * @param {number} [timeout] */ - private _activateTimer; + private _resetTimeout; /** * @private */