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 }