diff --git a/ChangeLog b/ChangeLog index 18db080f0..1bc36df57 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,17 @@ +2021-05-31 Richard Frith-Macdonald + + * Headers/GNUstepBase/GSTLS.h: ([GSTLSCredentials selfSigned:]) + * Source/GSTLS.m: Add new method to use certtool to generate a + key and a self-signed certificate. Use the new method to set up + server sessions if no certificate/key is configured. + * Headers/Foundation/NSPort.h: ([NSSocketPost setOptionsForTLS:]) + * Source/NSSocketPort.m: New methods to configure socket ports to + use TLS so that inter-host distributed object connections can be + securely encrypted. The class method should turn on encryption + for all subsequent connections using socket ports (the instance + method can be used to override the effects of the class method + for an individual instance). + 2021-05-23 Richard Frith-Macdonald * Source/GSStream.m: diff --git a/Headers/Foundation/NSPort.h b/Headers/Foundation/NSPort.h index aa9fc5687..a203f5205 100644 --- a/Headers/Foundation/NSPort.h +++ b/Headers/Foundation/NSPort.h @@ -208,6 +208,7 @@ GS_EXPORT_CLASS uint16_t portNum; /* TCP port in host byte order. */ SOCKET listener; NSMapTable *handles; /* Handles indexed by socket. */ + NSDictionary *tlsopts; /* TLS options */ #if defined(_WIN32) WSAEVENT eventListener; NSMapTable *events; @@ -273,6 +274,21 @@ GS_EXPORT_CLASS * Returns port number of underlying socket. */ - (uint16_t) portNumber; + +#if !NO_GNUSTEP +/** Sets the default options for use of TLS by socket ports.
+ * Setting nil (the default) means that TLS is not used.
+ * Setting an empty dictionary means that TLS is used with normal options. + */ ++ (void) setOptionsForTLS: (NSDictionary*)opts; + +/** Overrides the default options for use of TLS by the receiver.
+ * Setting nil (the default) means that TLS is not used.
+ * Setting an empty dictionary means that TLS is used with normal options.
+ * This method has no effect on network sessions which are already established. + */ +- (void) setOptionsForTLS: (NSDictionary*)opts; +#endif @end diff --git a/Headers/GNUstepBase/GSTLS.h b/Headers/GNUstepBase/GSTLS.h index 8e8b27130..60933bb0b 100644 --- a/Headers/GNUstepBase/GSTLS.h +++ b/Headers/GNUstepBase/GSTLS.h @@ -175,6 +175,7 @@ GS_EXPORT_CLASS certificateKeyPassword: (NSString*)cp asClient: (BOOL)client debug: (BOOL)debug; ++ (GSTLSCredentials*) selfSigned: (BOOL)debug; - (gnutls_certificate_credentials_t) credentials; - (GSTLSPrivateKey*) key; - (GSTLSCertificateList*) list; diff --git a/Source/GSTLS.m b/Source/GSTLS.m index 2ddd37822..02a526a74 100644 --- a/Source/GSTLS.m +++ b/Source/GSTLS.m @@ -35,8 +35,10 @@ #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" @@ -1091,6 +1093,89 @@ static NSMutableDictionary *credentialsCache = 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) + { + 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(); + NSTask *task; + NSString *tmpPath; + + path = [path stringByAppendingPathComponent: [[NSUUID UUID] UUIDString]]; + keyPath = [path stringByAppendingPathExtension: @"key"]; + tmpPath = [path stringByAppendingPathExtension: @"tmp"]; + crtPath = [path stringByAppendingPathExtension: @"crt"]; + [tmp writeToFile: tmpPath atomically: NO]; + + task = [NSTask new]; + [task setLaunchPath: @"certtool"]; + [task setArguments: [NSArray arrayWithObjects: + @"--generate-privkey", @"--sec-param", @"high", @"--outfile", keyPath, + nil]]; + [task setStandardOutput: [NSFileHandle fileHandleWithNullDevice]]; + [task setStandardError: [NSFileHandle fileHandleWithNullDevice]]; + [task launch]; + [task waitUntilExit]; + RELEASE(task); + key = [NSData dataWithContentsOfFile: keyPath]; + + task = [NSTask new]; + [task setLaunchPath: @"certtool"]; + [task setArguments: [NSArray arrayWithObjects: + @"--generate-self-signed", @"--load-privkey", keyPath, + @"--template", tmpPath, @"--outfile", crtPath, + nil]]; + [task setStandardOutput: [NSFileHandle fileHandleWithNullDevice]]; + [task setStandardError: [NSFileHandle fileHandleWithNullDevice]]; + [task launch]; + [task waitUntilExit]; + crt = [NSData dataWithContentsOfFile: crtPath]; + + [mgr removeFileAtPath: keyPath handler: nil]; + [mgr removeFileAtPath: tmpPath handler: nil]; + [mgr removeFileAtPath: crtPath handler: nil]; + 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; + } + keyPath = standardizedPath(@"self-signed-key"); + [self setData: key forTLSFile: keyPath]; + crtPath = standardizedPath(@"self-signed-crt-"); + [self setData: crt forTLSFile: crtPath]; + } + + 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 @@ -1322,6 +1407,7 @@ static NSMutableDictionary *credentialsCache = nil; return [c autorelease]; } + - (void) dealloc { if (nil != name) @@ -1539,17 +1625,18 @@ retrieve_callback(gnutls_session_t session, { if (nil != (self = [super init])) { - NSString *ca; - NSString *dca; - NSString *rv; - NSString *drv; - NSString *cf; - NSString *ck; - NSString *cp; - NSString *pri; - NSString *str; - BOOL trust; - BOOL verify; + GSTLSCredentials *cr; + NSString *ca; + NSString *dca; + NSString *rv; + NSString *drv; + NSString *cf; + NSString *ck; + NSString *cp; + NSString *pri; + NSString *str; + BOOL trust; + BOOL verify; created = [NSDate timeIntervalSinceReferenceDate]; opts = [options copy]; @@ -1632,28 +1719,41 @@ retrieve_callback(gnutls_session_t session, } setup = YES; - ca = [opts objectForKey: GSTLSCAFile]; - dca = caFile; - rv = [opts objectForKey: GSTLSRevokeFile]; - drv = revokeFile; cf = [opts objectForKey: GSTLSCertificateFile]; - ck = [opts objectForKey: GSTLSCertificateKeyFile]; - cp = [opts objectForKey: GSTLSCertificateKeyPassword]; - credentials = [[GSTLSCredentials credentialsFromCAFile: ca - defaultCAFile: dca - revokeFile: rv - defaultRevokeFile: drv - certificateFile: cf - certificateKeyFile: ck - certificateKeyPassword: cp - asClient: outgoing - debug: debug] retain]; + 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 (nil == credentials) + if (cr) { - [self release]; + ASSIGN(credentials, cr); + } + else + { + RELEASE(self); return nil; } diff --git a/Source/NSSocketPort.m b/Source/NSSocketPort.m index 746ef9163..db2ec53fa 100644 --- a/Source/NSSocketPort.m +++ b/Source/NSSocketPort.m @@ -26,6 +26,7 @@ #define EXPOSE_NSPort_IVARS 1 #define EXPOSE_NSSocketPort_IVARS 1 #import "GNUstepBase/GSLock.h" +#import "GNUstepBase/GSTLS.h" #import "Foundation/NSArray.h" #import "Foundation/NSNotification.h" #import "Foundation/NSNotificationQueue.h" @@ -122,6 +123,10 @@ */ static uint32_t maxDataLength = 32 * 1024 * 1024; +/* Options for TLS encryption of connections + */ +static NSDictionary *tlsOptions; + #if 0 #define M_LOCK(X) {NSDebugMLLog(@"GSTcpHandleLock",@"lock %@ in %@",X,[NSThread currentThread]); [X lock];} #define M_UNLOCK(X) {NSDebugMLLog(@"GSTcpHandleLock",@"unlock %@ in %@",X,[NSThread currentThread]); [X unlock];} @@ -200,9 +205,15 @@ typedef enum { GS_H_CONNECTED // Currently connected. } GSHandleState; +@interface NSSocketPort (GSTcpHandle) +- (NSDictionary*) optionsForTLS; +@end + @interface GSTcpHandle : NSObject { SOCKET desc; /* File descriptor for I/O. */ + NSData *cData; /* Connection data. */ + unsigned cLength; /* Connection data written. */ unsigned wItem; /* Index of item being written. */ NSMutableData *wData; /* Data object being written. */ unsigned wLength; /* Ammount written so far. */ @@ -223,7 +234,6 @@ typedef enum { BOOL inReplyMode; /* Indicate when have addEvent self */ BOOL readyToSend; /* Indicate when send */ #endif - @public NSRecursiveLock *myLock; /* Lock for this handle. */ BOOL caller; /* Did we connect to other end? */ @@ -232,6 +242,7 @@ typedef enum { NSSocketPort *sendPort; struct sockaddr sockAddr; /* Far end of connection. */ NSString *defaultAddress; + GSTLSSession *session; /* Session for encryption. */ } + (GSTcpHandle*) handleWithDescriptor: (SOCKET)d; @@ -253,9 +264,95 @@ typedef enum { - (NSSocketPort*) sendPort; - (void) setState: (GSHandleState)s; - (GSHandleState) state; +- (void) setupTLS: (NSSocketPort*)opts asClient: (BOOL)outgoing; @end +#if defined(HAVE_GNUTLS) + +/* Callback to allow the TLS code to pull data from the remote system. + * If the operation fails, this sets the error number. + */ +static ssize_t +GSTLSHandlePull(gnutls_transport_ptr_t handle, void *buffer, size_t len) +{ + ssize_t result = 0; + GSTcpHandle *tls = (GSTcpHandle*)handle; + int descriptor = [tls descriptor]; + + result = recv(descriptor, buffer, len, 0); + if (result < 0) + { +#if HAVE_GNUTLS_TRANSPORT_SET_ERRNO + if (tls->session && tls->session->session) + { + int e; + +#if defined(_WIN32) + /* For windows, we need to map winsock errors to unix ones that + * gnutls understands. + */ + e = WSAGetLastError(); + if (WSAEWOULDBLOCK == e) + { + e = EAGAIN; + } + else if (WSAEINTR == e) + { + e = EINTR; + } +#else + e = errno; +#endif + gnutls_transport_set_errno (tls->session->session, e); + } +#endif + } + return result; +} + +/* Callback to allow the TLS code to push data to the remote system. + * If the operation fails, this sets the error number. + */ +static ssize_t +GSTLSHandlePush(gnutls_transport_ptr_t handle, const void *buffer, size_t len) +{ + ssize_t result = 0; + GSTcpHandle *tls = (GSTcpHandle*)handle; + int descriptor = [tls descriptor]; + + result = send(descriptor, buffer, len, 0); + if (result < 0) + { +#if HAVE_GNUTLS_TRANSPORT_SET_ERRNO + if (tls->session && tls->session->session) + { + int e; + +#if defined(_WIN32) + /* For windows, we need to map winsock errors to unix ones that + * gnutls understands. + */ + e = WSAGetLastError(); + if (WSAEWOULDBLOCK == e) + { + e = EAGAIN; + } + else if (WSAEINTR == e) + { + e = EINTR; + } +#else + e = errno; +#endif + gnutls_transport_set_errno(tls->session->session, e); + } +#endif + } + return result; +} +#endif + /* * Utility functions for encoding and decoding ports. */ @@ -696,6 +793,7 @@ static Class runLoopClass; { [self finalize]; DESTROY(defaultAddress); + DESTROY(cData); DESTROY(rData); DESTROY(rItems); DESTROY(wMsgs); @@ -724,6 +822,11 @@ static Class runLoopClass; - (void) finalize { [self invalidate]; + if (session) + { + [session disconnect: NO]; + DESTROY(session); + } #if defined(_WIN32) if (event != WSA_INVALID_EVENT) { @@ -804,7 +907,21 @@ static Class runLoopClass; * Now try to fill the buffer with data. */ bytes = [rData mutableBytes]; - res = recv(desc, bytes + rLength, want - rLength, 0); + if (session) + { + if ([session handshake]) + { + res = [session read: bytes + rLength length: want - rLength]; + } + else + { + res = -1; + } + } + else + { + res = recv(desc, bytes + rLength, want - rLength, 0); + } if (res <= 0) { if (res == 0) @@ -1070,7 +1187,7 @@ static Class runLoopClass; if (shouldDispatch == YES) { NSPortMessage *pm; - NSSocketPort *rp = [self recvPort]; + NSSocketPort *rp = [self recvPort]; pm = [portMessageClass allocWithZone: NSDefaultMallocZone()]; pm = [pm initWithSendPort: [self sendPort] @@ -1123,29 +1240,72 @@ static Class runLoopClass; } else { - NSData *d = newDataWithEncodedPort([self recvPort]); + unsigned l; + const void *b; - len = send(desc, [d bytes], [d length], 0); - if (len == (int)[d length]) - { - ASSIGN(defaultAddress, GSPrivateSockaddrHost(&sockAddr)); - NSDebugMLLog(@"GSTcpHandle", - @"wrote %d bytes on 0x%"PRIxPTR, len, (NSUInteger)self); - state = GS_H_CONNECTED; + if (nil == cData) + { + ASSIGN(cData, newDataWithEncodedPort([self recvPort])); + cLength = 0; + } + b = [cData bytes]; + l = [cData length]; + + if (session) + { + if ([session handshake]) + { + len = [session write: b + cLength length: l - cLength]; + } + else + { + len = -1; + } } else { - state = GS_H_UNCON; - NSLog(@"connect write attempt failed - %@", - [NSError _last]); + len = send(desc, b + cLength, l - cLength, 0); + } + if (len <= 0) + { +#ifdef _WIN32 + if (WSAGetLastError() != WSAEINTR + && WSAGetLastError() != WSAEWOULDBLOCK) +#else + if (errno != EINTR && errno != EAGAIN) +#endif /* !_WIN32 */ + { + DESTROY(cData); + state = GS_H_UNCON; + NSLog(@"connect write attempt failed - %@", + [NSError _last]); + return; + } +#ifdef _WIN32 + if (WSAGetLastError() == WSAEWOULDBLOCK) + { + readyToSend = NO; + } +#endif /* !_WIN32 */ + } + else + { + cLength += len; + if (cLength >= l) + { + DESTROY(cData); + ASSIGN(defaultAddress, GSPrivateSockaddrHost(&sockAddr)); + NSDebugMLLog(@"GSTcpHandle", + @"wrote %d bytes on 0x%"PRIxPTR, len, (NSUInteger)self); + state = GS_H_CONNECTED; + } } - RELEASE(d); } } else { int res; - unsigned l; + unsigned l; const void *b; if (wData == nil) @@ -1165,7 +1325,21 @@ static Class runLoopClass; } b = [wData bytes]; l = [wData length]; - res = send(desc, b + wLength, l - wLength, 0); + if (session) + { + if ([session handshake]) + { + res = [session write: b + wLength length: l - wLength]; + } + else + { + res = -1; + } + } + else + { + res = send(desc, b + wLength, l - wLength, 0); + } if (res < 0) { #ifdef _WIN32 @@ -1424,6 +1598,20 @@ static Class runLoopClass; state = s; } +- (void) setupTLS: (NSSocketPort*)port asClient: (BOOL)outgoing +{ + NSDictionary *opts; + + if (nil == session && (opts = [port optionsForTLS]) != nil) + { + session = [[GSTLSSession alloc] initWithOptions: opts + direction: outgoing + transport: (void*)self + push: GSTLSHandlePush + pull: GSTLSHandlePull]; + } +} + - (GSHandleState) state { return state; @@ -1700,6 +1888,7 @@ static Class tcpPortClass; NSMapInsert(thePorts, (void*)aHost, (void*)port); NSDebugMLLog(@"NSPort", @"Created speaking port: %@", port); } + [port setOptionsForTLS: tlsOptions]; } else { @@ -1712,6 +1901,11 @@ static Class tcpPortClass; return port; } ++ (void) setOptionsForTLS: (NSDictionary*)options +{ + ASSIGNCOPY(tlsOptions, options); +} + - (void) addHandle: (GSTcpHandle*)handle forSend: (BOOL)send { M_LOCK(myLock); @@ -1774,6 +1968,7 @@ static Class tcpPortClass; NSFreeMapTable(handles); handles = 0; } + DESTROY(tlsopts); DESTROY(host); TEST_RELEASE(address); DESTROY(myLock); @@ -1959,6 +2154,7 @@ static Class tcpPortClass; } else { + [handle setupTLS: self asClient: YES]; [recvPort addHandle: handle forSend: NO]; } M_UNLOCK(myLock); @@ -2085,6 +2281,16 @@ static Class tcpPortClass; return NO; } +- (NSDictionary*) optionsForTLS +{ + NSDictionary *opts; + + M_LOCK(myLock); + opts = RETAIN(tlsopts); + M_UNLOCK(myLock); + return AUTORELEASE(opts); +} + - (uint16_t) portNumber { return portNum; @@ -2142,8 +2348,8 @@ static Class tcpPortClass; handle = [GSTcpHandle handleWithDescriptor: desc]; memcpy(&handle->sockAddr, &sockAddr, sizeof(sockAddr)); ASSIGN(handle->defaultAddress, GSPrivateSockaddrHost(&sockAddr)); - [handle setState: GS_H_ACCEPT]; + [handle setupTLS: self asClient: NO]; [self addHandle: handle forSend: NO]; } } @@ -2437,4 +2643,13 @@ static Class tcpPortClass; return sent; } +/** Sets the TLS options for network connections created by this port. + */ +- (void) setOptionsForTLS: (NSDictionary*)options +{ + M_LOCK(myLock); + ASSIGNCOPY(tlsopts, options); + M_UNLOCK(myLock); +} + @end