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 }