/** Implementation for GSTLS classes for GNUStep Copyright (C) 2012 Free Software Foundation, Inc. Written by: Richard Frith-Macdonald Date: 2101 This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 31 Milk Street #960789 Boston, MA 02196 USA. */ #import "common.h" #import "Foundation/NSArray.h" #import "Foundation/NSBundle.h" #import "Foundation/NSData.h" #import "Foundation/NSDictionary.h" #import "Foundation/NSEnumerator.h" #import "Foundation/NSFileManager.h" #import "Foundation/NSHost.h" #import "Foundation/NSException.h" #import "Foundation/NSLock.h" #import "Foundation/NSNotification.h" #import "Foundation/NSProcessInfo.h" #import "Foundation/NSStream.h" #import "Foundation/NSTask.h" #import "Foundation/NSThread.h" #import "Foundation/NSUserDefaults.h" #import "Foundation/NSUUID.h" #import "GNUstepBase/GSTLS.h" #import "GSPrivate.h" @interface NSString(gnutlsFileSystemRepresentation) - (const char*) gnutlsFileSystemRepresentation; @end @implementation NSString(gnutlsFileSystemRepresentation) - (const char*) gnutlsFileSystemRepresentation { #if defined(_WIN32) const unichar *buf = (const unichar*)[self fileSystemRepresentation]; int len = 0; NSString *str; const char *result; while (buf[len] > 0) { len++; } str = [[NSString alloc] initWithBytes: buf length: len * 2 encoding: NSUnicodeStringEncoding]; result = [str UTF8String]; RELEASE(str); return result; #else return [self fileSystemRepresentation]; #endif } @end #if defined(HAVE_GNUTLS) static NSString * standardizedPath(NSString *path) { if (0 == [path length]) { return nil; // Not a path } if (NO == [path isAbsolutePath]) { path = [[[NSFileManager defaultManager] currentDirectoryPath] stringByAppendingPathComponent: path]; } return [path stringByStandardizingPath]; } #if GNUTLS_VERSION_NUMBER <= 0x020b00 /* Set up locking callbacks for gcrypt so that it will be thread-safe. */ static int gcry_mutex_init(void **priv) { NSLock *lock = [NSLock new]; *priv = (void*)lock; return 0; } static int gcry_mutex_destroy(void **lock) { [((NSLock*)*lock) release]; return 0; } static int gcry_mutex_lock(void **lock) { [((NSLock*)*lock) lock]; return 0; } static int gcry_mutex_unlock(void **lock) { [((NSLock*)*lock) unlock]; return 0; } static struct gcry_thread_cbs gcry_threads_other = { GCRY_THREAD_OPTION_DEFAULT, NULL, gcry_mutex_init, gcry_mutex_destroy, gcry_mutex_lock, gcry_mutex_unlock }; #endif static void GSTLSLog(int level, const char *msg) { NSLog(@"%s", msg); } /* The caFile variable holds the location of the file containing the default * certificate authorities to be used by our system. * The hard-coded value is a file in the GSTLS folder of the base library * resource bundle, but this can be overridden by the GS_TLS_CA_FILE * environment variable, which in turn will be overridden by the GSTLSCAFile * user default string. */ static NSString *caFile = nil; // GSTLS/ca-certificates.crt /* The caRevoke variable holds the location of the file containing the default * certificate revocation list to be used by our system. * The hard-coded value is a file in the GSTLS folder of the base library * resource bundle, but this can be overridden by the GS_TLS_REVOKE * environment variable, which in turn will be overridden by the GSTLSRevokeFile * user default string. */ static NSString *revokeFile = nil; // GSTLS/revoke.crl /* The verifyClient variable tells us if connections from a remote server * should (by default) provide a client certificate which we verify against * our trusted authorities. * The hard-coded value can be overridden by the GS_TLS_VERIFY_C environment * variable, which in turn will be overridden by the GSTLSVerifyClient user * default string. * A GSTLSVerify option set for a specific session overrides this default */ static BOOL verifyClient = NO; /* The verifyServer variable tells us if outgoing connections (as a client) * to a remote server should (by default) verify that server's certificate * against our trusted authorities. * The hard-coded value can be overridden by the GS_TLS_VERIFY_S environment * variable, which in turn will be overridden by the GSTLSVerifyServer user * default string. * A GSTLSVerify option set for a specific session overrides this default */ static BOOL verifyServer = NO; /* The globalDebug variable turns on gnutls debug. The hard-code value is * overridden by GS_TLS_DEBUG, which in turn can be overridden by the * GSTLSDebug user default. This is an integer debug level with higher * values producing more debug output. Usually levels above 1 are too * verbose and not useful unless you have the gnutls source code to hand. * NB. The GSTLSDebug session option is a boolean to turn on extra debug for * a particular session to be produced on verification failure. */ static int globalDebug = 0; /* Defines the default priority list. */ static NSString *priority = nil; static gnutls_anon_client_credentials_t anoncred; /* This class is used to ensure that the GNUTLS system is initialised * and thread-safe. */ @implementation GSTLSObject static NSLock *certificateListLock = nil; static NSMutableDictionary *certificateListCache = nil; static NSLock *credentialsLock = nil; static NSMutableDictionary *credentialsCache = nil; static NSLock *fileLock = nil; static NSMutableDictionary *fileMap = nil; static NSLock *paramsLock = nil; static NSMutableDictionary *paramsCache = nil; static NSLock *privateKeyLock = nil; static NSMutableDictionary *privateKeyCache0 = nil; static NSMutableDictionary *privateKeyCache1 = nil; + (void) _defaultsChanged: (NSNotification*)n { NSBundle *bundle; NSUserDefaults *defs; NSDictionary *env; NSString *str; bundle = [NSBundle bundleForClass: [NSObject class]]; defs = [NSUserDefaults standardUserDefaults]; env = [[NSProcessInfo processInfo] environment]; str = [defs stringForKey: @"GSCipherList"]; if (nil != str) { GSOnceMLog(@"GSCipherList is no longer used, please try GSTLSPriority"); } str = [defs stringForKey: GSTLSPriority]; if (0 == [str length]) { str = nil; // nil or empty string resets to default } ASSIGN(priority, str); /* The GSTLSCAFile user default overrides the builtin value or the * GS_TLS_CA_FILE environment variable. */ str = [defs stringForKey: GSTLSCAFile]; if (nil == str) { /* Let the GS_TLS_CA_FILE environment variable override the * default certificate authority location. * Failing that, use the same environment variable as OpenSSL */ str = [env objectForKey: @"GS_TLS_CA_FILE"]; if (nil == str) { str = [env objectForKey: @"SSL_CERT_FILE"]; } if (nil == str) { str = [bundle pathForResource: @"ca-certificates" ofType: @"crt" inDirectory: @"GSTLS"]; } } str = standardizedPath(str); ASSIGN(caFile, str); /* The GSTLSRevokeFile user default overrides the builtin value or the * GS_TLS_REVOKE environment variable. */ str = [defs stringForKey: GSTLSRevokeFile]; if (nil == str) { /* Let the GS_TLS_REVOKE environment variable override the * default revocation list location. */ str = [env objectForKey: @"GS_TLS_REVOKE"]; if (nil == str) { str = [bundle pathForResource: @"revoke" ofType: @"crl" inDirectory: @"GSTLS"]; } } str = standardizedPath(str); ASSIGN(revokeFile, str); str = [defs stringForKey: @"GSTLSVerifyClient"]; if (nil == str) { str = [env objectForKey: @"GS_TLS_VERIFY_C"]; } verifyClient = [str boolValue]; str = [defs stringForKey: @"GSTLSVerifyServer"]; if (nil == str) { str = [env objectForKey: @"GS_TLS_VERIFY_S"]; } verifyServer = [str boolValue]; str = [defs stringForKey: GSTLSDebug]; if (nil == str) { str = [env objectForKey: @"GS_TLS_DEBUG"]; } globalDebug = [str intValue]; if (globalDebug < 0) { globalDebug = 0; } gnutls_global_set_log_level(globalDebug); } + (void) atExit { if ([NSObject shouldCleanUp]) { DESTROY(certificateListLock); DESTROY(certificateListCache); DESTROY(credentialsLock); DESTROY(credentialsCache); DESTROY(fileLock); DESTROY(fileMap); DESTROY(paramsLock); DESTROY(paramsCache); DESTROY(privateKeyLock); DESTROY(privateKeyCache0); DESTROY(privateKeyCache1); } } + (NSData*) dataForTLSFile: (NSString*)fileName { NSData *result; if (NO == [fileName isKindOfClass: [NSString class]]) { [NSException raise: NSInvalidArgumentException format: @"[GSTLS+dataForTLSFile:] called with bad file name"]; } [fileLock lock]; NS_DURING { result = [[fileMap objectForKey: fileName] retain]; } NS_HANDLER { [fileLock unlock]; result = nil; [localException raise]; } NS_ENDHANDLER [fileLock unlock]; if (nil == result) { return [NSData dataWithContentsOfFile: fileName]; } return [result autorelease]; } + (void) initialize { if ([GSTLSObject class] == self) { static BOOL beenHere = NO; if (beenHere == NO) { beenHere = YES; [self registerAtExit]; fileLock = [NSLock new]; fileMap = [NSMutableDictionary new]; [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(_defaultsChanged:) name: NSUserDefaultsDidChangeNotification object: nil]; #if GNUTLS_VERSION_NUMBER <= 0x020b00 /* Make gcrypt thread-safe */ gcry_control(GCRYCTL_SET_THREAD_CBS, &gcry_threads_other); #endif /* Initialise gnutls */ gnutls_global_init(); /* Allocate global credential information for anonymous tls */ gnutls_anon_allocate_client_credentials(&anoncred); /* Enable gnutls logging via NSLog */ gnutls_global_set_log_function(GSTLSLog); [self _defaultsChanged: nil]; } } } + (void) setData: (NSData*)data forTLSFile: (NSString*)fileName { fileName = standardizedPath(fileName); if (nil != data && NO == [data isKindOfClass: [NSData class]]) { [NSException raise: NSInvalidArgumentException format: @"[GSTLS+setData:forTLSFile:] called with bad data"]; } if (NO == [fileName isKindOfClass: [NSString class]]) { [NSException raise: NSInvalidArgumentException format: @"[GSTLS+setData:forTLSFile:] called with bad file"]; } [fileLock lock]; NS_DURING { if (data == nil) { [fileMap removeObjectForKey: fileName]; } else { [fileMap setObject: data forKey: fileName]; } } NS_HANDLER { [fileLock unlock]; [localException raise]; } NS_ENDHANDLER [fileLock unlock]; } @end @implementation GSTLSDHParams static NSTimeInterval paramsWhen = 0.0; static BOOL paramsGenerating = NO; static GSTLSDHParams *paramsCurrent = nil; + (GSTLSDHParams*) current { GSTLSDHParams *p; [paramsLock lock]; if (nil == paramsCurrent) { if (NO == paramsGenerating) { [paramsLock unlock]; [self generate]; [paramsLock lock]; } while (nil == paramsCurrent) { [paramsLock unlock]; [NSThread sleepForTimeInterval: 0.2]; [paramsLock lock]; } } p = [paramsCurrent retain]; [paramsLock unlock]; return [p autorelease]; } + (void) generate { GSTLSDHParams *p; [paramsLock lock]; if (YES == paramsGenerating) { [paramsLock unlock]; return; } paramsGenerating = YES; [paramsLock unlock]; p = [GSTLSDHParams new]; /* Generate Diffie-Hellman parameters - for use with DHE * kx algorithms. When short bit length is used, it might * be wise to regenerate parameters often. */ gnutls_dh_params_init(&p->params); gnutls_dh_params_generate2 (p->params, 2048); [paramsLock lock]; [paramsCurrent release]; paramsCurrent = p; paramsWhen = [NSDate timeIntervalSinceReferenceDate]; paramsGenerating = NO; [paramsLock unlock]; } + (void) housekeeping: (NSNotification*)n { NSEnumerator *enumerator; NSString *key; NSTimeInterval now; now = [NSDate timeIntervalSinceReferenceDate]; [paramsLock lock]; enumerator = [[paramsCache allKeys] objectEnumerator]; while (nil != (key = [enumerator nextObject])) { GSTLSDHParams *p; p = [paramsCache objectForKey: key]; if (now - p->when > 300.0) { [paramsCache removeObjectForKey: key]; } } /* Regenerate DH params once per day, perfoming generation in another * thread since it's likely to be rather slow. */ if (nil != paramsCurrent && NO == paramsGenerating && paramsWhen > 24.0 * 60.0 * 60.0) { [NSThread detachNewThreadSelector: @selector(generate) toTarget: self withObject: nil]; } [paramsLock unlock]; } + (void) initialize { if (nil == paramsLock) { paramsLock = [NSLock new]; paramsWhen = [NSDate timeIntervalSinceReferenceDate]; paramsCache = [NSMutableDictionary new]; [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(housekeeping:) name: @"GSHousekeeping" object: nil]; } } + (GSTLSDHParams*) paramsFromFile: (NSString*)f { GSTLSDHParams *p; if (nil == f) { return nil; } f = standardizedPath(f); [paramsLock lock]; p = [[paramsCache objectForKey: f] retain]; [paramsLock unlock]; if (nil == p) { NSData *data; int ret; gnutls_datum_t datum; data = [[self class] dataForTLSFile: f]; if (nil == data) { NSLog(@"Unable to read DF params file '%@'", f); return nil; } datum.data = (unsigned char*)[data bytes]; datum.size = (unsigned int)[data length]; p = [self alloc]; p->when = [NSDate timeIntervalSinceReferenceDate]; p->path = [f copy]; gnutls_dh_params_init(&p->params); ret = gnutls_dh_params_import_pkcs3(p->params, &datum, GNUTLS_X509_FMT_PEM); if (ret < 0) { NSLog(@"Unable to parse DH params file '%@': %s", p->path, gnutls_strerror(ret)); [p release]; return nil; } [paramsLock lock]; [paramsCache setObject: p forKey: p->path]; [paramsLock unlock]; } return [p autorelease]; } - (void) dealloc { gnutls_dh_params_deinit(params); [super dealloc]; } - (gnutls_dh_params_t) params { return params; } @end @implementation GSTLSCertificateList + (void) certInfo: (gnutls_x509_crt_t)cert to: (NSMutableString*)str { #if GNUTLS_VERSION_NUMBER >= 0x030507 gnutls_datum_t dn; #else char dn[1024]; size_t dn_size = sizeof(dn); #endif char serial[40]; size_t serial_size = sizeof(serial); time_t expiret; time_t activet; int algo; unsigned int bits; int i; [str appendFormat: _(@"- Certificate version: #%d\n"), gnutls_x509_crt_get_version(cert)]; #if GNUTLS_VERSION_NUMBER >= 0x030507 if (GNUTLS_E_SUCCESS == gnutls_x509_crt_get_dn3(cert, &dn, 0)) { [str appendFormat: @"- Certificate DN: %@\n", [NSString stringWithUTF8String: (const char*)dn.data]]; gnutls_free(dn.data); } if (GNUTLS_E_SUCCESS == gnutls_x509_crt_get_issuer_dn3(cert, &dn, 0)) { [str appendFormat: _(@"- Certificate Issuer's DN: %@\n"), [NSString stringWithUTF8String: (const char*)dn.data]]; gnutls_free(dn.data); } #else dn_size = sizeof(dn) - 1; gnutls_x509_crt_get_dn(cert, dn, &dn_size); dn[dn_size] = '\0'; [str appendFormat: @"- Certificate DN: %@\n", [NSString stringWithUTF8String: dn]]; dn_size = sizeof(dn) - 1; gnutls_x509_crt_get_issuer_dn(cert, dn, &dn_size); dn[dn_size] = '\0'; [str appendFormat: _(@"- Certificate Issuer's DN: %@\n"), [NSString stringWithUTF8String: dn]]; #endif activet = gnutls_x509_crt_get_activation_time(cert); [str appendFormat: _(@"- Certificate is valid since: %s"), ctime(&activet)]; expiret = gnutls_x509_crt_get_expiration_time(cert); [str appendFormat: _(@"- Certificate expires: %s"), ctime (&expiret)]; #if 0 { char digest[20]; size_t digest_size = sizeof(digest); if (gnutls_x509_fingerprint(GNUTLS_DIG_MD5, &cert_list[0], digest, &digest_size) >= 0) { [str appendString: _(@"- Certificate fingerprint: ")]; for (i = 0; i < digest_size; i++) { [str appendFormat: @"%.2x ", (unsigned char)digest[i]]; } [str appendString: @"\n"]; } } #endif if (gnutls_x509_crt_get_serial(cert, serial, &serial_size) >= 0) { [str appendString: _(@"- Certificate serial number: ")]; for (i = 0; i < serial_size; i++) { [str appendFormat: @"%.2x ", (unsigned char)serial[i]]; } [str appendString: @"\n"]; } [str appendString: _(@"- Certificate public key: ")]; algo = gnutls_x509_crt_get_pk_algorithm(cert, &bits); if (GNUTLS_PK_RSA == algo) { [str appendFormat: _(@"RSA - Modulus: %d bits\n"), bits]; } else if (GNUTLS_PK_DSA == algo) { [str appendFormat: _(@"DSA - Exponent: %d bits\n"), bits]; } else { [str appendString: _(@"UNKNOWN\n")]; } } /* Method to purge older lists from cache. */ + (void) housekeeping: (NSNotification*)n { NSEnumerator *enumerator; NSString *key; NSTimeInterval now; now = [NSDate timeIntervalSinceReferenceDate]; [certificateListLock lock]; enumerator = [[certificateListCache allKeys] objectEnumerator]; while (nil != (key = [enumerator nextObject])) { GSTLSCertificateList *list; list = [certificateListCache objectForKey: key]; if (now - list->when > 300.0) { [certificateListCache removeObjectForKey: key]; } } [certificateListLock unlock]; } + (void) initialize { if (nil == certificateListLock) { certificateListLock = [NSLock new]; certificateListCache = [NSMutableDictionary new]; [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(housekeeping:) name: @"GSHousekeeping" object: nil]; } } + (GSTLSCertificateList*) listFromFile: (NSString*)f { GSTLSCertificateList *l; if (nil == f) { return nil; } f = standardizedPath(f); [certificateListLock lock]; l = [[certificateListCache objectForKey: f] retain]; [certificateListLock unlock]; if (nil == l) { NSData *data; int ret; gnutls_datum_t datum; unsigned int count = 100; gnutls_x509_crt_t crts[count]; data = [[self class] dataForTLSFile: f]; if (nil == data) { NSLog(@"Unable to read certificate file '%@'", f); return nil; } datum.data = (unsigned char*)[data bytes]; datum.size = (unsigned int)[data length]; l = [self alloc]; l->when = [NSDate timeIntervalSinceReferenceDate]; l->path = [f copy]; ret = gnutls_x509_crt_list_import(crts, &count, &datum, GNUTLS_X509_FMT_PEM, // GNUTLS_X509_CRT_LIST_FAIL_IF_UNSORTED | GNUTLS_X509_CRT_LIST_IMPORT_FAIL_IF_EXCEED); if (ret < 0) { NSLog(@"Unable to parse certificate file '%@': %s", l->path, gnutls_strerror(ret)); [l release]; return nil; } if (count > 0) { time_t now = (time_t)[[NSDate date] timeIntervalSince1970]; unsigned int i; l->crts = malloc(sizeof(gnutls_x509_crt_t) * count); memcpy(l->crts, crts, sizeof(gnutls_x509_crt_t) * count); l->count = count; for (i = 0; i < count; i++) { time_t expiret = gnutls_x509_crt_get_expiration_time(crts[i]); time_t activet = gnutls_x509_crt_get_activation_time(crts[i]); if (expiret <= now) { NSLog(@"WARNING: at index %u in %@ ... expired at %s", i, l->path, ctime(&activet)); } if (activet > now) { NSLog(@"WARNING: at index %u in %@ ... not valid until %s", i, l->path, ctime(&activet)); } if (expiret <= now || activet > now) { NSMutableString *m; m = [NSMutableString stringWithCapacity: 2000]; [self certInfo: crts[i] to: m]; NSLog(@"%@", m); } } } [certificateListLock lock]; [certificateListCache setObject: l forKey: l->path]; [certificateListLock unlock]; } return [l autorelease]; } - (gnutls_x509_crt_t*) certificateList { return crts; } - (unsigned int) count { return count; } - (NSDate*) expiresAt { unsigned index = count; time_t expiret; if (index-- == 0) { return nil; } expiret = gnutls_x509_crt_get_expiration_time(crts[index]); if (expiret < 0) { return nil; } while (index > 0) { time_t t = gnutls_x509_crt_get_expiration_time(crts[--index]); if (t < 0) { return nil; } if (t < expiret) { expiret = t; } } return [NSDate dateWithTimeIntervalSince1970: expiret]; } - (NSDate*) expiresAt: (unsigned int)index { time_t expiret; if (count == 0 || index > count - 1) { return nil; } expiret = gnutls_x509_crt_get_expiration_time(crts[index]); if (expiret < 0) { return nil; } else { return [NSDate dateWithTimeIntervalSince1970: expiret]; } } - (void) dealloc { if (nil != path) { DESTROY(path); if (count > 0) { while (count-- > 0) { if (crts) gnutls_x509_crt_deinit(crts[count]); } if (crts) free(crts); } } [super dealloc]; } @end @implementation GSTLSPrivateKey /* Method to purge older keys from cache. */ + (void) housekeeping: (NSNotification*)n { NSEnumerator *outer; NSString *oKey; NSTimeInterval now; now = [NSDate timeIntervalSinceReferenceDate]; [privateKeyLock lock]; outer = [[privateKeyCache0 allKeys] objectEnumerator]; while (nil != (oKey = [outer nextObject])) { GSTLSPrivateKey *key; key = [privateKeyCache0 objectForKey: oKey]; if (now - key->when > 300.0) { [privateKeyCache0 removeObjectForKey: oKey]; } } outer = [[privateKeyCache1 allKeys] objectEnumerator]; while (nil != (oKey = [outer nextObject])) { NSMutableDictionary *m; NSEnumerator *inner; NSString *iKey; m = [privateKeyCache1 objectForKey: oKey]; inner = [[m allKeys] objectEnumerator]; while (nil != (iKey = [inner nextObject])) { GSTLSPrivateKey *key = [m objectForKey: iKey]; if (now - key->when > 300.0) { [m removeObjectForKey: iKey]; if (0 == [m count]) { [privateKeyCache1 removeObjectForKey: oKey]; } } } } [privateKeyLock unlock]; } + (void) initialize { if (nil == privateKeyLock) { privateKeyLock = [NSLock new]; privateKeyCache0 = [NSMutableDictionary new]; privateKeyCache1 = [NSMutableDictionary new]; [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(housekeeping:) name: @"GSHousekeeping" object: nil]; } } + (GSTLSPrivateKey*) keyFromFile: (NSString*)f withPassword: (NSString*)p { GSTLSPrivateKey *k; if (nil == f) { return nil; } f = standardizedPath(f); [privateKeyLock lock]; if (nil == p) { k = [privateKeyCache0 objectForKey: f]; } else { NSMutableDictionary *m; m = [privateKeyCache1 objectForKey: f]; if (nil == m) { k = nil; } else { k = [m objectForKey: p]; } } [k retain]; [privateKeyLock unlock]; if (nil == k) { NSData *data; int ret; gnutls_datum_t datum; data = [[self class] dataForTLSFile: f]; if (nil == data) { NSLog(@"Unable to read private key file '%@'", f); return nil; } datum.data = (unsigned char*)[data bytes]; datum.size = (unsigned int)[data length]; k = [self alloc]; k->when = [NSDate timeIntervalSinceReferenceDate]; k->path = [f copy]; k->password = [p copy]; gnutls_x509_privkey_init(&k->key); #ifdef HAVE_GNUTLS_X509_PRIVKEY_IMPORT2 /* This function can read openssl proprietory key format, * and uses the password if supplied. */ ret = gnutls_x509_privkey_import2(k->key, &datum, GNUTLS_X509_FMT_PEM, [k->password UTF8String], 0); #else if (nil == k->password) { ret = gnutls_x509_privkey_import(k->key, &datum, GNUTLS_X509_FMT_PEM); } else { ret = gnutls_x509_privkey_import_pkcs8(k->key, &datum, GNUTLS_X509_FMT_PEM, [k->password UTF8String], 0); } #endif if (ret < 0) { NSLog(@"Unable to parse private key file '%@': %s", k->path, gnutls_strerror(ret)); [k release]; return nil; } [privateKeyLock lock]; if (nil == k->password) { [privateKeyCache0 setObject: k forKey: k->path]; } else { NSMutableDictionary *m; m = [privateKeyCache1 objectForKey: f]; if (nil == m) { m = [NSMutableDictionary new]; [privateKeyCache1 setObject: m forKey: f]; [m release]; } [m setObject: k forKey: p]; } [privateKeyLock unlock]; } return [k autorelease]; } - (void) dealloc { if (nil != path) { DESTROY(path); DESTROY(password); gnutls_x509_privkey_deinit(key); } [super dealloc]; } - (gnutls_x509_privkey_t) key { return key; } @end @implementation GSTLSCredentials /* Method to purge older credentials from cache. */ + (void) housekeeping: (NSNotification*)n { NSEnumerator *enumerator; NSDictionary *key; NSTimeInterval now; now = [NSDate timeIntervalSinceReferenceDate]; [credentialsLock lock]; enumerator = [[credentialsCache allKeys] objectEnumerator]; while (nil != (key = [enumerator nextObject])) { GSTLSCredentials *cred; cred = [credentialsCache objectForKey: key]; if (now - cred->when > 300.0) { [credentialsCache removeObjectForKey: key]; } } [credentialsLock unlock]; } + (void) initialize { if (nil == credentialsLock) { credentialsLock = [NSLock new]; credentialsCache = [NSMutableDictionary new]; [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(housekeeping:) name: @"GSHousekeeping" object: nil]; } } + (GSTLSCredentials*) selfSigned: (BOOL)debug { NSString *crtPath = standardizedPath(@"self-signed-crt"); NSString *keyPath = standardizedPath(@"self-signed-key"); NSData *crt = [self dataForTLSFile: crtPath]; NSData *key = [self dataForTLSFile: keyPath]; if (nil == crt || nil == key) { ENTER_POOL static NSString *tmp = @"organization = SelfSigned\n" @"state = Example\n" @"country = EX\n" @"cn = SelfSigned\n" @"serial = 007\n" @"expiration_days = 730\n" @"dns_name = server.selfsigned.com\n" @"tls_www_server\n" @"encryption_key\n"; NSFileManager *mgr = [NSFileManager defaultManager]; NSString *path = NSTemporaryDirectory(); NSFileHandle *devNull = [NSFileHandle fileHandleWithNullDevice]; NSTask *task; NSString *tmpCrt; NSString *tmpKey; NSString *tmpTmp; path = [path stringByAppendingPathComponent: [[NSUUID UUID] UUIDString]]; tmpCrt = [path stringByAppendingPathExtension: @"crt"]; tmpKey = [path stringByAppendingPathExtension: @"key"]; tmpTmp = [path stringByAppendingPathExtension: @"tmp"]; [tmp writeToFile: tmpTmp atomically: NO]; task = [NSTask new]; [task setLaunchPath: @"certtool"]; [task setArguments: [NSArray arrayWithObjects: @"--generate-privkey", @"--sec-param", @"high", @"--outfile", tmpKey, nil]]; [task setStandardOutput: devNull]; [task setStandardError: devNull]; [task launch]; [task waitUntilExit]; RELEASE(task); key = [NSData dataWithContentsOfFile: tmpKey]; task = [NSTask new]; [task setLaunchPath: @"certtool"]; [task setArguments: [NSArray arrayWithObjects: @"--generate-self-signed", @"--load-privkey", tmpKey, @"--template", tmpTmp, @"--outfile", tmpCrt, nil]]; [task setStandardOutput: devNull]; [task setStandardError: devNull]; [task launch]; [task waitUntilExit]; RELEASE(task); crt = [NSData dataWithContentsOfFile: tmpCrt]; [mgr removeFileAtPath: tmpCrt handler: nil]; [mgr removeFileAtPath: tmpKey handler: nil]; [mgr removeFileAtPath: tmpTmp handler: nil]; if (key && crt) { [self setData: crt forTLSFile: crtPath]; [self setData: key forTLSFile: keyPath]; } LEAVE_POOL if (nil == key) { NSLog(@"Failed to make self-signed certificate key using 'certtool'"); return nil; } if (nil == crt) { NSLog(@"Failed to make self-signed certificate using 'certtool'"); return nil; } } return [self credentialsFromCAFile: nil defaultCAFile: nil revokeFile: nil defaultRevokeFile: nil certificateFile: crtPath certificateKeyFile: keyPath certificateKeyPassword: nil asClient: NO debug: debug]; } + (GSTLSCredentials*) credentialsFromCAFile: (NSString*)ca defaultCAFile: (NSString*)dca revokeFile: (NSString*)rv defaultRevokeFile: (NSString*)drv certificateFile: (NSString*)cf certificateKeyFile: (NSString*)ck certificateKeyPassword: (NSString*)cp asClient: (BOOL)client debug: (BOOL)debug { GSTLSCredentials *c; NSMutableString *k; /* Build a unique key for the credentials based on all the * information used to build them (apart from password used * to load the key). */ k = [NSMutableString stringWithCapacity: 1024]; ca = standardizedPath(ca); if (nil != ca) [k appendString: ca]; [k appendString: @":"]; if (nil != dca) [k appendString: dca]; [k appendString: @":"]; rv = standardizedPath(rv); if (nil != rv) [k appendString: rv]; [k appendString: @":"]; if (nil != drv) [k appendString: drv]; [k appendString: @":"]; if (nil != cf) [k appendString: cf]; [k appendString: @":"]; if (nil != ck) [k appendString: ck]; [credentialsLock lock]; c = [credentialsCache objectForKey: k]; if (nil != c) { [c retain]; if (YES == debug) { NSLog(@"Re-used credentials %p for '%@'", c, k); } } [credentialsLock unlock]; if (nil == c) { c = [self new]; c->name = [k copy]; c->when = [NSDate timeIntervalSinceReferenceDate]; gnutls_certificate_allocate_credentials(&c->certcred); c->freeCred = YES; // Need to free on dealloc /* Set the default trusted authority certificates. */ if ([dca length] > 0) { const char *path; int ret; path = [dca gnutlsFileSystemRepresentation]; ret = gnutls_certificate_set_x509_trust_file(c->certcred, path, GNUTLS_X509_FMT_PEM); if (ret < 0) { NSLog(@"Problem loading trusted authorities from %@: %s", dca, gnutls_strerror(ret)); } else { if (ret > 0) { c->trust = YES; // Loaded at least one trusted CA } if (YES == debug) { NSLog(@"Default trusted authorities (from %@): %d", dca, ret); } } } /* Load any specified trusted authority certificates. */ if ([ca length] > 0) { const char *path; int ret; path = [dca gnutlsFileSystemRepresentation]; ret = gnutls_certificate_set_x509_trust_file(c->certcred, path, GNUTLS_X509_FMT_PEM); if (ret < 0) { NSLog(@"Problem loading trusted authorities from %@: %s", ca, gnutls_strerror(ret)); } else { if (ret > 0) { c->trust = YES; } else if (0 == ret) { NSLog(@"No certificates processed from %@", ca); } if (YES == debug) { NSLog(@"Trusted authorities (from %@): %d", ca, ret); } } } /* Load default revocation list. */ if ([drv length] > 0) { const char *path; int ret; path = [drv gnutlsFileSystemRepresentation]; ret = gnutls_certificate_set_x509_crl_file(c->certcred, path, GNUTLS_X509_FMT_PEM); if (ret < 0) { NSLog(@"Problem loading default revocation list from %@: %s", drv, gnutls_strerror(ret)); } else { if (YES == debug) { NSLog(@"Default revocations (from %@): %d", drv, ret); } } } /* Load any specified revocation list. */ if ([rv length] > 0) { const char *path; int ret; path = [rv gnutlsFileSystemRepresentation]; ret = gnutls_certificate_set_x509_crl_file(c->certcred, path, GNUTLS_X509_FMT_PEM); if (ret < 0) { NSLog(@"Problem loading revocation list from %@: %s", rv, gnutls_strerror(ret)); } else { if (0 == ret) { NSLog(@"No revocations processed from %@", rv); } if (YES == debug) { NSLog(@"Revocations (from %@): %d", rv, ret); } } } /* Get the key for our certificate .. if one is specified. */ if (nil != ck) { c->key = [[GSTLSPrivateKey keyFromFile: ck withPassword: cp] retain]; if (nil == c->key) { [c release]; return nil; } } /* Load our certificate (may be a list) if the file is specified. */ if (nil != cf) { c->list = [[GSTLSCertificateList listFromFile: cf] retain]; if (nil == c->list) { [c release]; return nil; } } /* If we have loaded a certificate, we add it to the credentials * using the certificate key so we can use it. */ if (nil != c->list) { int ret; ret = gnutls_certificate_set_x509_key(c->certcred, [c->list certificateList], [c->list count], [c->key key]); if (ret < 0) { c->freeCred = NO; // Already freed NSLog(@"Unable to set certificate for session: %s", gnutls_strerror(ret)); [c release]; return nil; } /* if (NO == client) { // FIXME ... if the server certificate required DH params ... c->dhParams = [[GSTLSDHParams current] retain]; gnutls_certificate_set_dh_params(c->certcred, [c->dhParams params]); } */ } if (YES == debug) { NSLog(@"%@ created credentials %p for '%@'", self, c, k); } [credentialsLock lock]; [credentialsCache setObject: c forKey: c->name]; [credentialsLock unlock]; } return [c autorelease]; } - (void) dealloc { if (nil != name) { if (YES == freeCred) { gnutls_certificate_free_credentials(certcred); } DESTROY(key); DESTROY(list); DESTROY(dhParams); DESTROY(name); } [super dealloc]; } - (gnutls_certificate_credentials_t) credentials { return certcred; } - (GSTLSPrivateKey*) key { return key; } - (GSTLSCertificateList*) list { return list; } - (BOOL) trust { return trust; } @end #if GNUTLS_VERSION_NUMBER >= 0x020C00 /* Callback used only when debug is enabled, to print the request for a * certificate and the response to that request. * NB. This function always returns the certificate set for the session * even if that certificate does not match the CAs or algorithms requested * by the server. This differs from the default behavior which is for the * library code to only return a certificate matching the request. * So, the logging of a returned certificate does not guarantee that the * certificate is acceptable to the server. */ static int retrieve_callback(gnutls_session_t session, const gnutls_datum_t *req_ca_rdn, int nreqs, const gnutls_pk_algorithm_t *sign_algos, int sign_algos_length, gnutls_retr2_st *st) { GSTLSSession *s = gnutls_session_get_ptr(session); char issuer_dn[256]; int i; int ret; size_t len; /* Print the server's trusted CAs */ if (nreqs > 0) NSLog(@"- Server's trusted authorities:"); else NSLog(@"- Server did not send us any trusted authorities names."); /* print the names (if any) */ for (i = 0; i < nreqs; i++) { len = sizeof(issuer_dn); ret = gnutls_x509_rdn_get(&req_ca_rdn[i], issuer_dn, &len); if (ret >= 0) { NSLog(@" [%d]: %s", i, issuer_dn); } } /* Select a certificate and return it. * The certificate must be of any of the "sign algorithms" * supported by the server. */ if (gnutls_certificate_type_get(session) == GNUTLS_CRT_X509) { GSTLSCredentials *credentials = [s credentials]; GSTLSPrivateKey *key = [credentials key]; GSTLSCertificateList *list = [credentials list]; int count = (int)[list count]; gnutls_x509_crt_t *crts = [list certificateList]; NSMutableString *m; m = [NSMutableString stringWithCapacity: 2000]; for (i = 0; i < count; i++) { [GSTLSCertificateList certInfo: crts[i] to: m]; } if (0 == count) { [m appendString: @"None."]; } NSLog(@"Certificates retrieved for sending to peer -\n%@", m); st->cert_type = GNUTLS_CRT_X509; st->ncerts = count; st->cert.x509 = crts; st->key.x509 = [key key]; return 0; } else { NSLog(@"Certificates retrieved for sending to peer -\n" @"None: not a request for an X509 certificate."); return -1; } } #endif @implementation GSTLSSession + (GSTLSSession*) sessionWithOptions: (NSDictionary*)options direction: (BOOL)isOutgoing transport: (void*)ioHandle push: (GSTLSIOW)pushFunc pull: (GSTLSIOR)pullFunc { GSTLSSession *sess; sess = [[self alloc] initWithOptions: options direction: isOutgoing transport: ioHandle push: pushFunc pull: pullFunc]; return [sess autorelease]; } - (BOOL) active { return active; } - (NSTimeInterval) age { return [NSDate timeIntervalSinceReferenceDate] - created; } - (GSTLSCredentials*) credentials { return credentials; } - (void) dealloc { [self finalize]; DESTROY(opts); DESTROY(credentials); DESTROY(problem); DESTROY(issuer); DESTROY(owner); [super dealloc]; } - (BOOL) debug { return debug; } - (BOOL) disconnect: (BOOL)reusable { BOOL ok = YES; if (YES == active || YES == handshake) { int result; active = NO; handshake = NO; if (NO == reusable) { /* Since the connection is not reusable, we need only try once. */ result = gnutls_bye(session, GNUTLS_SHUT_WR); } else { NSTimeInterval start; /* Attempting to do a clean shutdown on a reusable connection, * so we keep retrying for a little while to let the other end * close down cleanly. */ start = [NSDate timeIntervalSinceReferenceDate]; do { result = gnutls_bye(session, GNUTLS_SHUT_RDWR); } while ((GNUTLS_E_AGAIN == result || GNUTLS_E_INTERRUPTED == result) && ([NSDate timeIntervalSinceReferenceDate] - start) < 10.0); } if (result < 0) { ok = NO; } } if (YES == setup) { setup = NO; gnutls_db_remove_session(session); gnutls_deinit(session); } return ok; } - (void) finalize { [self disconnect: NO]; [super finalize]; } - (id) initWithOptions: (NSDictionary*)options direction: (BOOL)isOutgoing transport: (void*)ioHandle push: (GSTLSIOW)pushFunc pull: (GSTLSIOR)pullFunc { if (nil != (self = [super init])) { GSTLSCredentials *cr; NSString *ca; NSString *dca; NSString *rv; NSString *drv; NSString *cf; NSString *ck; NSString *cp; NSString *pri; NSString *str; BOOL trust; BOOL verify; /* Set this early because it is needed in debug output during init. */ handle = ioHandle; created = [NSDate timeIntervalSinceReferenceDate]; opts = [options copy]; outgoing = isOutgoing ? YES : NO; if (YES == outgoing) { verify = verifyServer; // Verify connection to remote server } else { verify = verifyClient; // Verify certificate of remote client } str = [opts objectForKey: GSTLSVerify]; if (nil != str) { verify = [str boolValue]; } debug = (globalDebug > 0) ? YES : NO; if (NO == debug) { debug = [[opts objectForKey: GSTLSDebug] boolValue]; } /* Now initialise session and set it up. It's simplest to always * allocate a credentials structure at this point (and get rid of * it when the session is disconnected) too. */ if (YES == outgoing) { gnutls_init(&session, GNUTLS_CLIENT); str = [opts objectForKey: GSTLSServerName]; if ([str length] > 0) { const char *ptr = [str UTF8String]; unsigned len = strlen(ptr); int ret; ret = gnutls_server_name_set(session, GNUTLS_NAME_DNS, ptr, len); if (YES == debug) { if (ret < 0) { NSLog(@"%p %@: failed '%s'", handle, GSTLSServerName, gnutls_strerror(ret)); } else { NSLog(@"%p %@: set to '%s'", handle, GSTLSServerName, ptr); } } } else if (YES == debug) { NSLog(@"%p %@: not set", handle, GSTLSServerName); } } else { gnutls_init(&session, GNUTLS_SERVER); if (NO == verify) { /* We don't want to demand/verify the client certificate, * but we still ask the other end to send it so that higher * level code can see what distinguished names are in it. */ gnutls_certificate_server_set_request(session, GNUTLS_CERT_REQUEST); } else { /* We request the client certificate and require them client * end to send it (if not, we don't allow the session). */ gnutls_certificate_server_set_request(session, GNUTLS_CERT_REQUIRE); } } setup = YES; cf = [opts objectForKey: GSTLSCertificateFile]; if (nil == cf && NO == outgoing) { /* Server with no certiticate supplied: generate self signed one. */ cr = [GSTLSCredentials selfSigned: debug]; } else { ca = [opts objectForKey: GSTLSCAFile]; dca = caFile; rv = [opts objectForKey: GSTLSRevokeFile]; drv = revokeFile; ck = [opts objectForKey: GSTLSCertificateKeyFile]; cp = [opts objectForKey: GSTLSCertificateKeyPassword]; cr = [GSTLSCredentials credentialsFromCAFile: ca defaultCAFile: dca revokeFile: rv defaultRevokeFile: drv certificateFile: cf certificateKeyFile: ck certificateKeyPassword: cp asClient: outgoing debug: debug]; } if (cr) { ASSIGN(credentials, cr); } else { RELEASE(self); return nil; } trust = [credentials trust]; if (YES == verify && NO == trust) { NSLog(@"You have requested that a TLS/SSL connection be to a remote" @" system with a verified certificate, but have provided no trusted" @" certificate authorities."); NSLog(@"If you did not use the GSTLSCAFile option to specify a file" @" containing certificate authorities for a session, and did not" @" specify a default file using the GSTLSCAFile user default or" @" the GS_TLS_CA_FILE environment variable, then the system will" @" have attempted to use the GSTLS/ca-certificates.crt file in the" @" gnustep-base resource bundle. Unfortunately, it has not been" @" possible to read any trusted certificate authorities from" @" these locations."); } pri = [opts objectForKey: NSStreamSocketSecurityLevelKey]; str = [opts objectForKey: GSTLSPriority]; if (nil == pri && nil == str) { str = priority; // Default setting } if (YES == [str isEqual: @"SSLv3"]) { pri = NSStreamSocketSecurityLevelSSLv3; str = nil; } else if (YES == [str isEqual: @"TLSv1"]) { pri = NSStreamSocketSecurityLevelTLSv1; str = nil; } if (nil == str) { if ([pri isEqual: NSStreamSocketSecurityLevelNone] == YES) { // pri = NSStreamSocketSecurityLevelNone; GSOnceMLog(@"NSStreamSocketSecurityLevelNone is insecure ..." @" not implemented"); DESTROY(self); return nil; } else if ([pri isEqual: NSStreamSocketSecurityLevelSSLv2] == YES) { // pri = NSStreamSocketSecurityLevelSSLv2; GSOnceMLog(@"NSStreamSocketSecurityLevelSSLv2 is insecure ..." @" not implemented"); DESTROY(self); return nil; } else if ([pri isEqual: NSStreamSocketSecurityLevelSSLv3] == YES) { #if GNUTLS_VERSION_NUMBER < 0x020C00 const int proto_prio[2] = { GNUTLS_SSL3, 0 }; gnutls_protocol_set_priority(session, proto_prio); #else gnutls_priority_set_direct(session, "NORMAL:-VERS-TLS-ALL:+VERS-SSL3.0", NULL); #endif GSOnceMLog(@"NSStreamSocketSecurityLevelSSLv3 is insecure ..." @" please change your code to stop using it"); } else if ([pri isEqual: NSStreamSocketSecurityLevelTLSv1] == YES) { #if GNUTLS_VERSION_NUMBER < 0x020C00 const int proto_prio[4] = { #if defined(GNUTLS_TLS1_2) GNUTLS_TLS1_2, #endif GNUTLS_TLS1_1, GNUTLS_TLS1_0, 0 }; gnutls_protocol_set_priority(session, proto_prio); #else gnutls_priority_set_direct(session, "NORMAL:-VERS-SSL3.0:+VERS-TLS-ALL", NULL); #endif } else { #if GNUTLS_VERSION_NUMBER < 0x020C00 gnutls_set_default_priority(session); #else /* By default we disable SSL3.0 as the 'POODLE' attack (Oct 2014) * renders it insecure. */ gnutls_priority_set_direct(session, "NORMAL:-VERS-SSL3.0", NULL); #endif } } else { #if GNUTLS_VERSION_NUMBER < 0x020C00 gnutls_set_default_priority(session); #else /* By default we disable SSL3.0 as the 'POODLE' attack (Oct 2014) * renders it insecure. */ const char *err_pos; if (gnutls_priority_set_direct(session, [str UTF8String], &err_pos)) { NSLog(@"Invalid GSTLSPriority: %s", err_pos); NSLog(@"Falling back to NORMAL:-VERS-SSL3.0"); gnutls_priority_set_direct(session, "NORMAL:-VERS-SSL3.0", NULL); } #endif } /* Set certificate credentials for this session. */ gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE, [credentials credentials]); #if GNUTLS_VERSION_NUMBER >= 0x020C00 if (YES == outgoing && YES == debug) { /* Set a callback to log handling of a request (from the server) * for the client certificate. The callback always returns the * certificate set for this session, even if that does not match * the server's request. */ gnutls_certificate_set_retrieve_function( [credentials credentials], retrieve_callback); } #endif /* Set transport layer to use */ #if GNUTLS_VERSION_NUMBER < 0x020C00 gnutls_transport_set_lowat(session, 0); #endif gnutls_transport_set_pull_function(session, pullFunc); gnutls_transport_set_push_function(session, pushFunc); gnutls_transport_set_ptr(session, (gnutls_transport_ptr_t)ioHandle); gnutls_session_set_ptr(session, (void*)self); } return self; } - (BOOL) handshake { int ret; if (YES == active || NO == setup) { return YES; // Handshake completed or impossible. } handshake = YES; ret = gnutls_handshake(session); if (ret < 0) { if (gnutls_error_is_fatal(ret)) { NSString *p; p = [NSString stringWithFormat: @"%s", gnutls_strerror(ret)]; /* We want to differentiate between errors which are usually * due to the remote end not expecting to be using TLS/SSL, * and errors which are caused by other interoperability * issues. The first sort are not normally worth reporting. */ if (ret == GNUTLS_E_UNEXPECTED_PACKET_LENGTH || ret == GNUTLS_E_FATAL_ALERT_RECEIVED || ret == GNUTLS_E_DECRYPTION_FAILED #ifdef GNUTLS_E_PREMATURE_TERMINATION || ret == GNUTLS_E_PREMATURE_TERMINATION #endif || ret == GNUTLS_E_UNSUPPORTED_VERSION_PACKET) { NSString *extra = nil; switch (ret) { case GNUTLS_E_FATAL_ALERT_RECEIVED: extra = @"The TLS protocol does not tell us why the remote" @" end sent an alert, but the most common problem during" @" handshake is a cipher mismatch"; break; case GNUTLS_E_UNEXPECTED_PACKET_LENGTH: case GNUTLS_E_PREMATURE_TERMINATION: extra = @"Most often this is due to the remote end not" @" expecting/supporting TLS"; break; } if (extra) { p = [p stringByAppendingFormat: @"\n%@", extra]; } ASSIGN(problem, p); if (YES == debug) { NSLog(@"%p in handshake: %@", handle, p); } } else { ASSIGN(problem, p); NSLog(@"%p in handshake: %@", handle, p); } [self disconnect: NO]; return YES; // Failed ... not active. } else { return NO; // Non-fatal error needs a retry. } } else { NSString *str; BOOL shouldVerify = NO; active = YES; // The TLS session is now active. handshake = NO; // Handshake is over. if (YES == outgoing) { shouldVerify = verifyServer; // Verify remote server certificate? } else { shouldVerify = verifyClient; // Verify remote client certificate? } str = [opts objectForKey: GSTLSVerify]; if (nil != str) { shouldVerify = [str boolValue]; } if (globalDebug > 1) { NSLog(@"%p trying verify:\n%@", handle, [self sessionInfo]); } ret = [self verify]; if (ret < 0) { if (globalDebug > 1 || (YES == shouldVerify && globalDebug > 0) || YES == [[opts objectForKey: GSTLSDebug] boolValue]) { NSLog(@"%p unable to verify SSL connection - %s", handle, gnutls_strerror(ret)); NSLog(@"%p %@", handle, [self sessionInfo]); } if (YES == shouldVerify) { [self disconnect: NO]; } } else { if (globalDebug > 1) { NSLog(@"%p succeeded verify:\n%@", handle, [self sessionInfo]); } } return YES; // Handshake complete } } - (NSString*) issuer { return issuer; } - (NSString*) owner { return owner; } - (size_t) pending { return gnutls_record_check_pending(session); } - (NSString*) problem { return problem; } - (NSInteger) read: (void*)buf length: (NSUInteger)len { int result = gnutls_record_recv(session, buf, len); if (result < 0) { NSString *p; if (GNUTLS_E_AGAIN == result) { errno = EAGAIN; // Need to retry. } else if (GNUTLS_E_INTERRUPTED == result) { errno = EINTR; // Need to retry } else if (gnutls_error_is_fatal(result)) { p = [NSString stringWithFormat: @"%s", gnutls_strerror(result)]; ASSIGN(problem, p); if (YES == debug) { NSLog(@"%p in tls read: %@", handle, p); } if (EAGAIN == errno || EINTR == errno) { errno = EBADF; // Fatal ... don't retry } } else { if (GNUTLS_E_WARNING_ALERT_RECEIVED == result) { if (YES == debug) { p = [NSString stringWithFormat: @"%s", gnutls_alert_get_name(gnutls_alert_get(session))]; NSLog(@"%p in tls read: %@", handle, p); } } errno = EAGAIN; // Need to retry. } result = -1; #if defined(_WIN32) /* Windows specific code expects to use winsock functions for error * codes rather than looking at errno, so we must translate a few. */ if (EAGAIN == errno) WSASetLastError(WSAEWOULDBLOCK); else if (EINTR == errno) WSASetLastError(WSAEINTR); else WSASetLastError(errno); #endif } return result; } - (NSInteger) write: (const void*)buf length: (NSUInteger)len { int result = gnutls_record_send(session, buf, len); if (result < 0) { if (GNUTLS_E_AGAIN == result) { errno = EAGAIN; // Need to retry. } else if (GNUTLS_E_INTERRUPTED == result) { errno = EINTR; // Need to retry } else if (gnutls_error_is_fatal(result)) { NSString *p; p = [NSString stringWithFormat: @"%s", gnutls_strerror(result)]; ASSIGN(problem, p); if (YES == debug) { NSLog(@"%p in tls write: %@", handle, p); } if (EAGAIN == errno || EINTR == errno) { errno = EBADF; // Fatal ... don't retry } } else { errno = EAGAIN; // Need to retry. } result = -1; #if defined(_WIN32) /* Windows specific code expects to use winsock functions for error * codes rather than looking at errno, so we must translate a few. */ if (EAGAIN == errno) WSASetLastError(WSAEWOULDBLOCK); else if (EINTR == errno) WSASetLastError(WSAEINTR); else WSASetLastError(errno); #endif } return result; } /* Copied/based on the public domain code provided by gnutls * to print the session ... I've left in details for features * we don't yet support. */ - (NSString*) sessionInfo { NSMutableString *str; const char *tmp; gnutls_credentials_type_t cred; gnutls_kx_algorithm_t kx; int dhe = 0; #if defined(XXX_ECDH) /* At some point we may want to implement ecdh */ int ecdh = 0; #endif str = [NSMutableString stringWithCapacity: 2000]; /* get the key exchange's algorithm name */ kx = gnutls_kx_get(session); tmp = gnutls_kx_get_name(kx); [str appendFormat: _(@"- Key Exchange: %s\n"), tmp]; /* Check the authentication type used and switch to the appropriate. */ cred = gnutls_auth_get_type(session); switch (cred) { case GNUTLS_CRD_IA: [str appendString: _(@"- TLS/IA session\n")]; break; case GNUTLS_CRD_SRP: #ifdef ENABLE_SRP [str appendFormat: _(@"- SRP session with username %s\n"), gnutls_srp_server_get_username(session)]; #endif break; case GNUTLS_CRD_PSK: #if 0 /* This returns NULL in server side. */ if (gnutls_psk_client_get_hint(session) != NULL) { [str appendFormat: _(@"- PSK authentication. PSK hint '%s'\n"), gnutls_psk_client_get_hint(session)]; } /* This returns NULL in client side. */ if (gnutls_psk_server_get_username(session) != NULL) { [str appendFormat: _(@"- PSK authentication. Connected as '%s'\n"), gnutls_psk_server_get_username(session)]; } if (GNUTLS_KX_ECDHE_PSK == kx) { ecdh = 1; } else if (GNUTLS_KX_DHE_PSK == kx) { dhe = 1; } #endif break; case GNUTLS_CRD_ANON: /* anonymous authentication */ #if 0 [str appendFormat: _(@"- Anonymous authentication.\n")]; if (GNUTLS_KX_ANON_ECDH == kx) { ecdh = 1; } else if (GNUTLS_KX_ANON_DH == kx) { dhe = 1; } #endif break; case GNUTLS_CRD_CERTIFICATE: /* certificate authentication */ /* Check if we have been using ephemeral Diffie-Hellman. */ if (GNUTLS_KX_DHE_RSA == kx || GNUTLS_KX_DHE_DSS == kx) { dhe = 1; } #if defined(XXX_ECDH) if (GNUTLS_KX_ECDHE_RSA == kx || GNUTLS_KX_ECDHE_ECDSA == kx) { dhe = 0; ecdh = 1; } #endif tmp = gnutls_certificate_type_get_name( gnutls_certificate_type_get(session)); [str appendFormat: _(@"- Authentication using certificate type: %s\n"), tmp]; break; } /* switch */ #if defined(XXXECDH) if (ecdh != 0) { [str appendFormat: _(@"- Ephemeral ECDH using curve %s\n"), gnutls_ecc_curve_get_name(gnutls_ecc_curve_get(session))]; } #endif if (dhe != 0) { [str appendFormat: _(@"- Ephemeral DH using prime of %d bits\n"), gnutls_dh_get_prime_bits(session)]; } /* print the protocol's name (eg TLS 1.0) */ tmp = gnutls_protocol_get_name(gnutls_protocol_get_version(session)); [str appendFormat: _(@"- Protocol: %s\n"), tmp]; /* print the certificates of the peer. */ if (gnutls_certificate_type_get(session) == GNUTLS_CRT_X509) { unsigned int cert_list_size = 0; const gnutls_datum_t *cert_list; gnutls_x509_crt_t cert; cert_list = gnutls_certificate_get_peers(session, &cert_list_size); if (0 == cert_list_size) { [str appendString: _(@"- Peer provided no certificate.\n")]; } else { int cert_num; for (cert_num = 0; cert_num < cert_list_size; cert_num++) { gnutls_x509_crt_init(&cert); /* NB. the list of peer certificate is in memory in native * format (DER) rather than the normal file format (PEM). */ gnutls_x509_crt_import(cert, &cert_list[cert_num], GNUTLS_X509_FMT_DER); [str appendFormat: _(@"- Certificate %d info:\n"), cert_num]; [GSTLSCertificateList certInfo: cert to: str]; gnutls_x509_crt_deinit(cert); } } } else { tmp = gnutls_certificate_type_get_name( gnutls_certificate_type_get(session)); [str appendFormat: _(@"- Certificate Type: %s\n"), tmp]; } /* print the name of the cipher used. * eg 3DES. */ tmp = gnutls_cipher_get_name(gnutls_cipher_get(session)); [str appendFormat: _(@"- Cipher: %s\n"), tmp]; /* Print the MAC algorithms name. * eg SHA1 */ tmp = gnutls_mac_get_name(gnutls_mac_get(session)); [str appendFormat: _(@"- MAC: %s\n"), tmp]; return str; } - (int) verify { NSArray *names; NSString *str; unsigned int status; const gnutls_datum_t *cert_list; unsigned int cert_list_size; int ret; gnutls_x509_crt_t cert; /* This verification function uses the trusted CAs in the credentials * structure. So you must have installed one or more CA certificates. */ ret = gnutls_certificate_verify_peers2(session, &status); if (ret < 0) { str = [NSString stringWithFormat: @"TLS verification: error %s", gnutls_strerror(ret)]; ASSIGN(problem, str); if (YES == debug) NSLog(@"%p %@", handle, problem); return GNUTLS_E_CERTIFICATE_ERROR; } if (YES == debug) { if (status & GNUTLS_CERT_SIGNER_NOT_FOUND) NSLog(@"%p TLS verification: certificate hasn't got a known issuer.", handle); if (status & GNUTLS_CERT_REVOKED) NSLog(@"%p TLS verification: certificate has been revoked.", handle); #if defined(GNUTLS_CERT_EXPIRED) if (status & GNUTLS_CERT_EXPIRED) NSLog(@"%p TLS verification: certificate has expired", handle); #endif #if defined(GNUTLS_CERT_NOT_ACTIVATED) if (status & GNUTLS_CERT_NOT_ACTIVATED) NSLog(@"%p TLS verification: certificate is not yet activated", handle); #endif } if (status & GNUTLS_CERT_INVALID) { ASSIGN(problem, @"TLS verification: remote certificate is not trusted."); if (YES == debug) NSLog(@"%p %@", handle, problem); return GNUTLS_E_CERTIFICATE_ERROR; } /* Up to here the process is the same for X.509 certificates and * OpenPGP keys. From now on X.509 certificates are assumed. This can * be easily extended to work with openpgp keys as well. */ if (gnutls_certificate_type_get(session) != GNUTLS_CRT_X509) { ASSIGN(problem, @"TLS verification: remote certificate not of the X509 type."); if (YES == debug) NSLog(@"%p %@", handle, problem); return GNUTLS_E_CERTIFICATE_ERROR; } if (gnutls_x509_crt_init(&cert) < 0) { ASSIGN(problem, @"TLS verification: error in certificate initialization"); gnutls_x509_crt_deinit(cert); if (YES == debug) NSLog(@"%p %@", handle, problem); return GNUTLS_E_CERTIFICATE_ERROR; } cert_list = gnutls_certificate_get_peers(session, &cert_list_size); if (cert_list == NULL) { ASSIGN(problem, @"TLS verification: no certificate from remote end!"); gnutls_x509_crt_deinit(cert); if (YES == debug) NSLog(@"%p %@", handle, problem); return GNUTLS_E_CERTIFICATE_ERROR; } if (gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER) < 0) { ASSIGN(problem, @"TLS verification: error parsing certificate"); gnutls_x509_crt_deinit(cert); if (YES == debug) NSLog(@"%p %@", handle, problem); return GNUTLS_E_CERTIFICATE_ERROR; } else { #if GNUTLS_VERSION_NUMBER >= 0x030507 gnutls_datum_t dn; if (GNUTLS_E_SUCCESS == gnutls_x509_crt_get_dn3(cert, &dn, 0)) { ASSIGN(owner, [NSString stringWithUTF8String: (const char*)dn.data]); gnutls_free(dn.data); } if (GNUTLS_E_SUCCESS == gnutls_x509_crt_get_issuer_dn3(cert, &dn, 0)) { ASSIGN(issuer, [NSString stringWithUTF8String: (const char*)dn.data]); gnutls_free(dn.data); } #else char dn[1024]; size_t dn_size; /* Get certificate owner and issuer */ dn_size = sizeof(dn)-1; gnutls_x509_crt_get_dn(cert, dn, &dn_size); dn[dn_size] = '\0'; ASSIGN(owner, [NSString stringWithUTF8String: dn]); dn_size = sizeof(dn)-1; gnutls_x509_crt_get_issuer_dn(cert, dn, &dn_size); dn[dn_size] = '\0'; ASSIGN(issuer, [NSString stringWithUTF8String: dn]); #endif } str = [opts objectForKey: GSTLSRemoteHosts]; if (nil == str) { names = nil; } else { /* The string is a comma separated list of permitted host names. */ names = [str componentsSeparatedByString: @","]; } if (nil != names) { NSEnumerator *enumerator = [names objectEnumerator]; BOOL found = NO; NSString *name; while (nil != (name = [enumerator nextObject])) { if (0 == gnutls_x509_crt_check_hostname(cert, [name UTF8String])) { found = YES; break; } } if (NO == found) { str = [NSString stringWithFormat: @"TLS verification: certificate's owner does not match '%@'", names]; ASSIGN(problem, str); gnutls_x509_crt_deinit(cert); if (YES == debug) NSLog(@"%p %@", handle, problem); return GNUTLS_E_CERTIFICATE_ERROR; } } gnutls_x509_crt_deinit(cert); names = [opts objectForKey: GSTLSIssuers]; if ([names isKindOfClass: [NSArray class]]) { if (nil == issuer || NO == [names containsObject: issuer]) { str = [NSString stringWithFormat: @"TLS verification: certificate's issuer does not match '%@'", names]; ASSIGN(problem, str); if (YES == debug) NSLog(@"%p %@", handle, problem); return GNUTLS_E_CERTIFICATE_ERROR; } } names = [opts objectForKey: GSTLSOwners]; if ([names isKindOfClass: [NSArray class]]) { if (nil == owner || NO == [names containsObject: owner]) { str = [NSString stringWithFormat: @"TLS verification: certificate's owner does not match '%@'", names]; ASSIGN(problem, str); if (YES == debug) NSLog(@"%p %@", handle, problem); return GNUTLS_E_CERTIFICATE_ERROR; } } return 0; // Verified } @end #endif