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