1 /******************************************************************************
2 
3     Task-based implementation of an SSL connection
4 
5     Copyright:
6       Copyright (c) 2018 dunnhumby Germany GmbH. All rights reserved
7 
8 *******************************************************************************/
9 
10 module ocean.net.ssl.SslClientConnection;
11 
12 import deimos.openssl.err;
13 import deimos.openssl.ssl;
14 
15 import ocean.meta.types.Qualifiers;
16 import ocean.text.util.StringC;
17 
18 
19 /***************************************************************************
20 
21     Global context used for all SSL connections
22 
23 ***************************************************************************/
24 
25 private SSL_CTX * globalSslContext;
26 
27 /***************************************************************************
28 
29     Class representing a single SSL client connection.
30     Can only be called from inside a Task.
31 
32 ***************************************************************************/
33 
34 public class SslClientConnection
35 {
36     import core.sys.posix.netdb : AF_UNSPEC, SOCK_STREAM;
37 
38     static import ocean.core.ExceptionDefinitions;
39     import ocean.io.select.protocol.task.TaskSelectClient;
40     import ocean.sys.Epoll: epoll_event_t;
41     import ocean.sys.socket.AddrInfo;
42     import ocean.sys.socket.model.ISocket;
43 
44     private alias epoll_event_t.Event Event;
45 
46 
47     /***************************************************************************
48 
49         Exception class thrown on errors.
50 
51     ***************************************************************************/
52 
53     public static class SslException :
54         ocean.core.ExceptionDefinitions.IOException
55     {
56         import ocean.core.Exception: ReusableExceptionImplementation;
57 
58         mixin ReusableExceptionImplementation!() ReusableImpl;
59 
60         /*******************************************************************
61 
62             Sets the exception instance.
63 
64             Params:
65                 file_path = path of the file
66                 func_name = name of the method that failed
67                 msg = message description of the error
68                 file = file where exception is thrown
69                 line = line where exception is thrown
70 
71         *******************************************************************/
72 
73         public typeof(this) set ( cstring host_path,
74                 string func_name,
75                 cstring msg,
76                 string file = __FILE__, long line = __LINE__)
77         {
78             this.error_num = error_num;
79             this.func_name = func_name;
80 
81             this.ReusableImpl.set(this.func_name, file, line)
82                 .fmtAppend(": {} {} on {}", this.func_name, msg, host_path);
83 
84             return this;
85         }
86     }
87 
88 
89     /***************************************************************************
90 
91         A minimal implementation of ISocket
92 
93     ***************************************************************************/
94 
95     private static class SimpleSocket : ISocket
96     {
97         public this ()
98         {
99             super (addrinfo.sizeof);
100         }
101 
102         public override void formatInfo ( ref char[] buf, bool io_error )
103         {
104 
105         }
106     }
107 
108     /***************************************************************************
109 
110         The socket which is used for the connection
111 
112     ***************************************************************************/
113 
114     private SimpleSocket socket;
115 
116 
117     /***************************************************************************
118 
119         The SSL connection
120 
121     ***************************************************************************/
122 
123     private SSL * sslHandle;
124 
125 
126     /***************************************************************************
127 
128         SelectClient used for blocking the calling Task
129 
130     ***************************************************************************/
131 
132     private TaskSelectClient select_client;
133 
134 
135     /***************************************************************************
136 
137         Instance of AddrInfor used for resolving host names
138 
139     ***************************************************************************/
140 
141     private AddrInfo addr_info;
142 
143 
144     /***********************************************************************
145 
146         Reusable exception instance
147 
148     ***********************************************************************/
149 
150     private SslException exception;
151 
152 
153     /***************************************************************************
154 
155         Constructor
156 
157     ***************************************************************************/
158 
159     public this ( )
160     {
161         this.addr_info = new AddrInfo();
162 
163         this.socket = new SimpleSocket();
164 
165         this.select_client = new TaskSelectClient(this.socket,
166             &this.socket.error);
167 
168         this.exception = new SslException;
169     }
170 
171 
172     /***************************************************************************
173 
174         Create an SSL connection. Blocks the calling task until the handshake
175         is complete.
176 
177         Params:
178             host_name = the name of the host
179             host_port = the port to use (eg "443" for an HTTPS connection)
180 
181         Throws:
182             IOException if the connection failed
183 
184     ***************************************************************************/
185 
186     public void connect ( cstring host_name, cstring host_port )
187     {
188         // Resolve the host
189 
190         // We should be able to use AddrInfo.getIp but cannot because it doesn't
191         // support AF_UNSPEC.
192 
193         auto addr_err = this.addr_info.get(host_name, host_port, AF_UNSPEC,
194             SOCK_STREAM, 0);
195 
196         if ( addr_err != 0 )
197         {
198             this.ssl_error(host_name, "connect", gai_strerror(addr_err));
199         }
200 
201         this.socket.socket(addr_info.info().ai_family,
202             addr_info.info().ai_socktype | SocketFlags.SOCK_NONBLOCK,
203             addr_info.info().ai_protocol);
204 
205         if ( !socket.connect(addr_info.info().ai_addr) )
206         {
207             this.error(host_name, "socket.connect", "Failed to connect");
208         }
209 
210         this.sslHandle = SSL_new(globalSslContext);
211 
212         if ( !this.sslHandle )
213         {
214             this.error(host_name, "connect", "sslNew failed");
215         }
216 
217         if (!SSL_set_fd(this.sslHandle, this.socket.fileHandle))
218         {
219             this.error(host_name, "connect", "sslSetFd failed");
220         }
221 
222         // Set it into "connect" state, ie it is a client, not a server.
223         // (For a server, SSL_set_accept_state would be called instead).
224 
225         SSL_set_connect_state(this.sslHandle);
226 
227         ERR_clear_error();
228 
229         int r;
230 
231         while ( (r = SSL_do_handshake(this.sslHandle)) != 1 )
232         {
233             int err = SSL_get_error(this.sslHandle, r);
234 
235             if ( err ==  SSL_ERROR_WANT_WRITE )
236             {
237                 this.select_client.ioWait(Event.EPOLLOUT);
238             }
239             else if (err == SSL_ERROR_WANT_READ)
240             {
241                 this.select_client.ioWait(Event.EPOLLIN);
242             }
243             else
244             {
245                 this.error(host_name, "connect", "Handshake failed");
246             }
247         }
248     }
249 
250 
251     /***************************************************************************
252 
253         Write a string to the SSL connection
254 
255         The calling task is blocked until the write has completed.
256 
257         Params:
258             request = the string to send
259 
260         Throws:
261             IOException if the write failed
262 
263     ***************************************************************************/
264 
265     public void write ( cstring request )
266     {
267         int w;
268 
269         while ( (w = SSL_write(this.sslHandle, request.ptr,
270             cast(int)request.length)) <= 0 )
271         {
272             auto err = SSL_get_error(this.sslHandle, w);
273 
274             // At any time, a renegotiation is possible, so a call to SSL_write
275             // can also cause read operations.
276 
277             if ( err ==  SSL_ERROR_WANT_WRITE )
278             {
279                 this.select_client.ioWait(Event.EPOLLOUT);
280             }
281             else if (err == SSL_ERROR_WANT_READ)
282             {
283                 this.select_client.ioWait(Event.EPOLLIN);
284             }
285             else
286             {
287                 // The write failed
288 
289                 this.ssl_error("", "write", ERR_reason_error_string(err));
290             }
291         }
292 
293         if ( w != request.length )
294         {
295             this.error("", "write", "Mismatch in number of bytes written");
296 
297         }
298     }
299 
300 
301     /***************************************************************************
302 
303         Reads a string from the SSL connection
304 
305         The calling task is blocked until the read has completed.
306 
307         Params:
308             buffer = array to store the string
309 
310         Returns:
311             a slice into buffer of the bytes which were read.
312 
313         Throws:
314             IOException if the read failed
315 
316     ***************************************************************************/
317 
318     public mstring read ( mstring buffer )
319     {
320         this.select_client.ioWait(Event.EPOLLIN);
321 
322         ERR_clear_error();
323 
324         int r;
325 
326         while ( (r = SSL_read(this.sslHandle, buffer.ptr,
327             cast(int)buffer.length)) <= 0 )
328         {
329             auto err = SSL_get_error(this.sslHandle, r);
330 
331             // At any time, a renegotiation is possible, so a call to SSL_read
332             // can also cause write operations.
333 
334             if ( err ==  SSL_ERROR_WANT_WRITE )
335             {
336                 this.select_client.ioWait(Event.EPOLLOUT);
337             }
338             else if (err == SSL_ERROR_WANT_READ)
339             {
340                 this.select_client.ioWait(Event.EPOLLIN);
341             }
342             else
343             {
344                 // The read failed
345 
346                 this.ssl_error("", "read", ERR_reason_error_string(err));
347             }
348 
349         }
350         return buffer[0 .. r];
351     }
352 
353 
354     /***************************************************************************
355 
356         Validate the X509 certificate.
357 
358         The relevant documents are RFC 5280 and RFC 6125.
359 
360         Params:
361             host_name = the name of the host
362 
363         Throws:
364             IOException if validation fails
365 
366     ***************************************************************************/
367 
368     public void validateCertificate (cstring host_name)
369     {
370         // Step 1. Verify that a server certificate was presented during
371         // negotiation.
372 
373         if (X509* cert = SSL_get_peer_certificate(this.sslHandle))
374         {
375             X509_free(cert); // Free the certificate immediately
376         }
377         else
378         {
379             this.error(host_name, "SSL_get_peer_certificate",
380                 "No certificate was presented");
381         }
382 
383         // Step 2: Verify the library default validation
384 
385         auto res = SSL_get_verify_result(this.sslHandle);
386 
387         if (res != 0)
388         {
389             this.ssl_error(host_name, "SSL_get_verify_result",
390                 ERR_reason_error_string(res));
391         }
392 
393         // Step 3: hostname verification.
394         // This was only necessary before OpenSSL 1.1.0.
395 
396         // Even if all three checks succeed, to properly ensure a secure
397         // connection would require something like Trust-On-First-Use, as used
398         // by SSH.
399     }
400 
401 
402     /*******************************************************************
403 
404         Throw a reusable IOException, with the provided
405         message, function name and error code.
406 
407         Params:
408             host_name = the host which was connected to
409             func_name = name of the method that failed
410             msg = message description of the error
411             file = file where exception is thrown
412             line = line where exception is thrown
413 
414     *******************************************************************/
415 
416     public void error ( cstring host_name, string func_name,
417             string msg = "", string file = __FILE__, long line = __LINE__ )
418     {
419         throw this.exception.set(host_name, func_name, msg, file, line);
420     }
421 
422 
423     /*******************************************************************
424 
425         Throw a reusable IOException, with the provided
426         message, function name and error code.
427 
428         Params:
429             host_name = the host which was connected to
430             func_name = name of the method that failed
431             msg = Pointer to a C string desribing the error
432             file = file where exception is thrown
433             line = line where exception is thrown
434 
435     *******************************************************************/
436 
437     public void ssl_error ( cstring host_name, string func_name,
438             const(char*) c_msg, string file = __FILE__, long line = __LINE__ )
439     {
440         throw this.exception.set(host_name, func_name, StringC.toDString(c_msg),
441             file, line);
442     }
443 }
444 
445 
446 
447 /*******************************************************************************
448 
449     Initializes SSL and creates a global SSL_CTX object
450 
451     This function must be called before any SSL clients can be created
452 
453     Params:
454         ca_path = a directory containing CA certificates in PEM format
455         ca_file = pointer to a file of CA certificates in PEM format, or
456                 null. The file can containe several CA certificates.
457 
458     Returns:
459 
460         0 if successful, otherwise returns an error code
461 
462 *******************************************************************************/
463 
464 
465 public ulong initializeSslAndCreateCtx ( const(char *) ca_path,
466     const(char *) ca_file = null )
467 {
468     globalSslContext = null;
469 
470     // Load the SSL v2 or v3 function table
471     auto method = TLS_method();
472 
473     if ( !method )
474     {
475         return ERR_get_error();
476     }
477 
478     // Create an SSL context to use for all future SSL operations
479 
480     globalSslContext = SSL_CTX_new(method);
481 
482     if ( !globalSslContext )
483     {
484         return ERR_get_error();
485     }
486 
487     SSL_CTX_set_verify(globalSslContext, SSL_VERIFY_PEER, null);
488 
489     SSL_CTX_set_verify_depth(globalSslContext, 5);
490 
491     // Remove the most problematic options. Because SSLv2 and SSLv3 have been
492     // removed, a TLSv1.0 handshake is used. Clients created from this context
493     // will accept TLSv1.0 and above. An added benefit of TLS 1.0 and above
494     // are TLS extensions like Server Name Indicatior (SNI).
495     long flags = SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3
496         | SSL_OP_NO_COMPRESSION;
497     long old_opts = SSL_CTX_set_options(globalSslContext, flags);
498 
499     // I found that SSL_get_verify_results returns error 20
500     // ("unable to get local issuer certificate")
501     // unless it has the path to the directory where CA keys are stored
502 
503     if ( !SSL_CTX_load_verify_locations(globalSslContext, ca_file,
504         ca_path) )
505     {
506         // Technically, this isn't a fatal error, but many subsequent
507         // operations will fail
508 
509         return ERR_get_error();
510     }
511 
512     // Success
513 
514     return 0;
515 }
516 
517 
518 
519 /*******************************************************************************
520 
521     Usage Exmple:
522 
523     Here is the fundamental code for an HTTPS client.
524     A proper HTTP client would need to parse the HTTP response header to
525     determine how many bytes should be read; this example will always trigger
526     an error after the final bytes are read.
527 
528 *******************************************************************************/
529 unittest
530 {
531     // Check that the code compiles.
532 
533     void test_ssl_compilation ()
534     {
535         .initializeSslAndCreateCtx("/etc/ssl/certs\0".ptr);
536 
537         auto client = new SslClientConnection;
538 
539         auto host = "en.wikipedia.org";
540         auto url_path = "/wiki/D_(programming_language)";
541 
542         try
543         {
544             client.connect(host, "443");
545             client.validateCertificate(host);
546 
547             auto request = "GET " ~ url_path ~ " HTTP/1.1\r\nHost: "
548                 ~ host ~ "\r\nConnection:close\r\n\r\n";
549 
550             client.write(request);
551 
552             char[500] buffer;
553 
554             while (true)
555             {
556                 auto result = client.read(buffer);
557 
558                 // The HTTP response header will arrive first, followed
559                 // by the data (a web page in this example)
560             }
561         }
562         catch (SslClientConnection.SslException e)
563         {
564         }
565         return;
566     }
567 }