| | @@ -46,11 +46,15 @@ |
| 46 | 46 | static int sslIsInit = 0; /* True after global initialization */ |
| 47 | 47 | static BIO *iBio = 0; /* OpenSSL I/O abstraction */ |
| 48 | 48 | static char *sslErrMsg = 0; /* Text of most recent OpenSSL error */ |
| 49 | 49 | static SSL_CTX *sslCtx; /* SSL context */ |
| 50 | 50 | static SSL *ssl; |
| 51 | | - |
| 51 | +static struct { /* Accept this SSL cert for this session only */ |
| 52 | + char *zHost; /* Subject or host name */ |
| 53 | + char *zHash; /* SHA2-256 hash of the cert */ |
| 54 | +} sException; |
| 55 | +static int sslNoCertVerify = 0; /* Do not verify SSL certs */ |
| 52 | 56 | |
| 53 | 57 | /* |
| 54 | 58 | ** Clear the SSL error message |
| 55 | 59 | */ |
| 56 | 60 | static void ssl_clear_errmsg(void){ |
| | @@ -224,38 +228,36 @@ |
| 224 | 228 | }while(!done); |
| 225 | 229 | sscanf(bbuf, "HTTP/1.%d %d", &httpVerMin, &rc); |
| 226 | 230 | blob_reset(&reply); |
| 227 | 231 | return rc; |
| 228 | 232 | } |
| 233 | + |
| 234 | +/* |
| 235 | +** Invoke this routine to disable SSL cert verification. After |
| 236 | +** this call is made, any SSL cert that the server provides will |
| 237 | +** be accepted. Communication will still be encrypted, but the |
| 238 | +** client has no way of knowing whether it is talking to the |
| 239 | +** real server or a man-in-the-middle imposter. |
| 240 | +*/ |
| 241 | +void ssl_disable_cert_verification(void){ |
| 242 | + sslNoCertVerify = 1; |
| 243 | +} |
| 229 | 244 | |
| 230 | 245 | /* |
| 231 | 246 | ** Open an SSL connection. The identify of the server is determined |
| 232 | 247 | ** as follows: |
| 233 | 248 | ** |
| 234 | | -** g.url.name Name of the server. Ex: www.fossil-scm.org |
| 249 | +** pUrlData->name Name of the server. Ex: www.fossil-scm.org |
| 250 | +** g.url.name Name of the proxy server, if proxying. |
| 235 | 251 | ** pUrlData->port TCP/IP port to use. Ex: 80 |
| 236 | 252 | ** |
| 237 | 253 | ** Return the number of errors. |
| 238 | 254 | */ |
| 239 | 255 | int ssl_open(UrlData *pUrlData){ |
| 240 | 256 | X509 *cert; |
| 241 | | - int hasSavedCertificate = 0; |
| 242 | | - int trusted = 0; |
| 243 | | - unsigned long e; |
| 244 | 257 | |
| 245 | 258 | ssl_global_init(); |
| 246 | | - |
| 247 | | - /* Get certificate for current server from global config and |
| 248 | | - * (if we have it in config) add it to certificate store. |
| 249 | | - */ |
| 250 | | - cert = ssl_get_certificate(pUrlData, &trusted); |
| 251 | | - if ( cert!=NULL ){ |
| 252 | | - X509_STORE_add_cert(SSL_CTX_get_cert_store(sslCtx), cert); |
| 253 | | - X509_free(cert); |
| 254 | | - hasSavedCertificate = 1; |
| 255 | | - } |
| 256 | | - |
| 257 | 259 | if( pUrlData->useProxy ){ |
| 258 | 260 | int rc; |
| 259 | 261 | char *connStr = mprintf("%s:%d", g.url.name, pUrlData->port); |
| 260 | 262 | BIO *sBio = BIO_new_connect(connStr); |
| 261 | 263 | free(connStr); |
| | @@ -325,70 +327,65 @@ |
| 325 | 327 | ssl_set_errmsg("No SSL certificate was presented by the peer"); |
| 326 | 328 | ssl_close(); |
| 327 | 329 | return 1; |
| 328 | 330 | } |
| 329 | 331 | |
| 330 | | - if( trusted<=0 && (e = SSL_get_verify_result(ssl)) != X509_V_OK ){ |
| 332 | + if( !sslNoCertVerify && SSL_get_verify_result(ssl)!=X509_V_OK ){ |
| 331 | 333 | char *desc, *prompt; |
| 332 | | - const char *warning = ""; |
| 333 | 334 | Blob ans; |
| 334 | 335 | char cReply; |
| 335 | 336 | BIO *mem; |
| 336 | 337 | unsigned char md[32]; |
| 337 | | - unsigned int mdLength = 31; |
| 338 | | - |
| 339 | | - mem = BIO_new(BIO_s_mem()); |
| 340 | | - X509_NAME_print_ex(mem, X509_get_subject_name(cert), 2, XN_FLAG_MULTILINE); |
| 341 | | - BIO_puts(mem, "\n\nIssued By:\n\n"); |
| 342 | | - X509_NAME_print_ex(mem, X509_get_issuer_name(cert), 2, XN_FLAG_MULTILINE); |
| 343 | | - BIO_puts(mem, "\n\nSHA1 Fingerprint:\n\n "); |
| 344 | | - if(X509_digest(cert, EVP_sha1(), md, &mdLength)){ |
| 345 | | - int j; |
| 346 | | - for( j = 0; j < mdLength; ++j ) { |
| 347 | | - BIO_printf(mem, " %02x", md[j]); |
| 348 | | - } |
| 349 | | - } |
| 350 | | - BIO_write(mem, "", 1); /* nul-terminate mem buffer */ |
| 351 | | - BIO_get_mem_data(mem, &desc); |
| 352 | | - |
| 353 | | - if( hasSavedCertificate ){ |
| 354 | | - warning = "WARNING: Certificate doesn't match the " |
| 355 | | - "saved certificate for this host!"; |
| 356 | | - } |
| 357 | | - prompt = mprintf("\nSSL verification failed: %s\n" |
| 358 | | - "Certificate received: \n\n%s\n\n%s\n" |
| 359 | | - "Either:\n" |
| 360 | | - " * verify the certificate is correct using the " |
| 361 | | - "SHA1 fingerprint above\n" |
| 362 | | - " * use the global ssl-ca-location setting to specify your CA root\n" |
| 363 | | - " certificates list\n\n" |
| 364 | | - "If you are not expecting this message, answer no and " |
| 365 | | - "contact your server\nadministrator.\n\n" |
| 366 | | - "Accept certificate for host %s (a=always/y/N)? ", |
| 367 | | - X509_verify_cert_error_string(e), desc, warning, |
| 368 | | - pUrlData->useProxy?pUrlData->hostname:pUrlData->name); |
| 369 | | - BIO_free(mem); |
| 370 | | - |
| 371 | | - prompt_user(prompt, &ans); |
| 372 | | - free(prompt); |
| 373 | | - cReply = blob_str(&ans)[0]; |
| 374 | | - blob_reset(&ans); |
| 375 | | - if( cReply!='y' && cReply!='Y' && cReply!='a' && cReply!='A') { |
| 376 | | - X509_free(cert); |
| 377 | | - ssl_set_errmsg("SSL certificate declined"); |
| 378 | | - ssl_close(); |
| 379 | | - return 1; |
| 380 | | - } |
| 381 | | - if( cReply=='a' || cReply=='A') { |
| 382 | | - if ( trusted==0 ){ |
| 383 | | - prompt_user("\nSave this certificate as fully trusted (a=always/N)? ", |
| 384 | | - &ans); |
| 385 | | - cReply = blob_str(&ans)[0]; |
| 386 | | - trusted = ( cReply=='a' || cReply=='A' ); |
| 387 | | - blob_reset(&ans); |
| 388 | | - } |
| 389 | | - ssl_save_certificate(pUrlData, cert, trusted); |
| 338 | + char zHash[32*2+1]; |
| 339 | + unsigned int mdLength = (int)sizeof(md); |
| 340 | + |
| 341 | + memset(md, 0, sizeof(md)); |
| 342 | + zHash[0] = 0; |
| 343 | + if( X509_digest(cert, EVP_sha256(), md, &mdLength) ){ |
| 344 | + int j; |
| 345 | + for(j=0; j<mdLength && j*2+1<sizeof(zHash); ++j){ |
| 346 | + zHash[j*2] = "0123456789abcdef"[md[j]>>4]; |
| 347 | + zHash[j*2+1] = "0123456789abcdef"[md[j]&0xf]; |
| 348 | + } |
| 349 | + zHash[j*2] = 0; |
| 350 | + } |
| 351 | + |
| 352 | + if( ssl_certificate_exception_exists(pUrlData, zHash) ){ |
| 353 | + /* Ignore the failure because an exception exists */ |
| 354 | + ssl_one_time_exception(pUrlData, zHash); |
| 355 | + }else{ |
| 356 | + /* Tell the user about the failure and ask what to do */ |
| 357 | + mem = BIO_new(BIO_s_mem()); |
| 358 | + BIO_puts(mem, " subject: "); |
| 359 | + X509_NAME_print_ex(mem, X509_get_subject_name(cert), 0, XN_FLAG_ONELINE); |
| 360 | + BIO_puts(mem, "\n issuer: "); |
| 361 | + X509_NAME_print_ex(mem, X509_get_issuer_name(cert), 0, XN_FLAG_ONELINE); |
| 362 | + BIO_printf(mem, "\n sha256: %s", zHash); |
| 363 | + BIO_get_mem_data(mem, &desc); |
| 364 | + |
| 365 | + prompt = mprintf("Unable to verify SSL cert from %s\n%s\n" |
| 366 | + "accept this cert and continue (y/N)? ", |
| 367 | + pUrlData->name, desc); |
| 368 | + BIO_free(mem); |
| 369 | + |
| 370 | + prompt_user(prompt, &ans); |
| 371 | + free(prompt); |
| 372 | + cReply = blob_str(&ans)[0]; |
| 373 | + blob_reset(&ans); |
| 374 | + if( cReply!='y' && cReply!='Y' ){ |
| 375 | + X509_free(cert); |
| 376 | + ssl_set_errmsg("SSL cert declined"); |
| 377 | + ssl_close(); |
| 378 | + return 1; |
| 379 | + } |
| 380 | + ssl_one_time_exception(pUrlData, zHash); |
| 381 | + prompt_user("remember this exception (y/N)? ", &ans); |
| 382 | + cReply = blob_str(&ans)[0]; |
| 383 | + if( cReply=='y' || cReply=='Y') { |
| 384 | + ssl_remember_certificate_exception(pUrlData, zHash); |
| 385 | + } |
| 386 | + blob_reset(&ans); |
| 390 | 387 | } |
| 391 | 388 | } |
| 392 | 389 | |
| 393 | 390 | /* Set the Global.zIpAddr variable to the server we are talking to. |
| 394 | 391 | ** This is used to populate the ipaddr column of the rcvfrom table, |
| | @@ -415,60 +412,53 @@ |
| 415 | 412 | X509_free(cert); |
| 416 | 413 | return 0; |
| 417 | 414 | } |
| 418 | 415 | |
| 419 | 416 | /* |
| 420 | | -** Save certificate to global config. |
| 417 | +** Remember that the cert with the given hash is a acceptable for |
| 418 | +** use with pUrlData->name. |
| 419 | +*/ |
| 420 | +LOCAL void ssl_remember_certificate_exception( |
| 421 | + UrlData *pUrlData, |
| 422 | + const char *zHash |
| 423 | +){ |
| 424 | + char *zName = mprintf("cert:%s", pUrlData->name); |
| 425 | + db_set(zName, zHash, 1); |
| 426 | + fossil_free(zName); |
| 427 | +} |
| 428 | + |
| 429 | +/* |
| 430 | +** Return true if the there exists a certificate exception for |
| 431 | +** pUrlData->name that matches the hash. |
| 421 | 432 | */ |
| 422 | | -void ssl_save_certificate(UrlData *pUrlData, X509 *cert, int trusted){ |
| 423 | | - BIO *mem; |
| 424 | | - char *zCert, *zHost; |
| 425 | | - |
| 426 | | - mem = BIO_new(BIO_s_mem()); |
| 427 | | - PEM_write_bio_X509(mem, cert); |
| 428 | | - BIO_write(mem, "", 1); /* nul-terminate mem buffer */ |
| 429 | | - BIO_get_mem_data(mem, &zCert); |
| 430 | | - zHost = mprintf("cert:%s", |
| 431 | | - pUrlData->useProxy ? pUrlData->hostname : pUrlData->name); |
| 432 | | - db_set(zHost, zCert, 1); |
| 433 | | - free(zHost); |
| 434 | | - zHost = mprintf("trusted:%s", |
| 435 | | - pUrlData->useProxy ? pUrlData->hostname : pUrlData->name); |
| 436 | | - db_set_int(zHost, trusted, 1); |
| 437 | | - free(zHost); |
| 438 | | - BIO_free(mem); |
| 433 | +LOCAL int ssl_certificate_exception_exists( |
| 434 | + UrlData *pUrlData, |
| 435 | + const char *zHash |
| 436 | +){ |
| 437 | + char *zName, *zValue; |
| 438 | + if( fossil_strcmp(sException.zHost,pUrlData->name)==0 |
| 439 | + && fossil_strcmp(sException.zHash,zHash)==0 |
| 440 | + ){ |
| 441 | + return 1; |
| 442 | + } |
| 443 | + zName = mprintf("cert:%s", pUrlData->name); |
| 444 | + zValue = db_get(zName,0); |
| 445 | + fossil_free(zName); |
| 446 | + return zValue!=0 && strcmp(zHash,zValue)==0; |
| 439 | 447 | } |
| 440 | 448 | |
| 441 | 449 | /* |
| 442 | | -** Get certificate for pUrlData->urlName from global config. |
| 443 | | -** Return NULL if no certificate found. |
| 450 | +** Remember zHash as an acceptable certificate for this session only. |
| 444 | 451 | */ |
| 445 | | -X509 *ssl_get_certificate(UrlData *pUrlData, int *pTrusted){ |
| 446 | | - char *zHost, *zCert; |
| 447 | | - BIO *mem; |
| 448 | | - X509 *cert; |
| 449 | | - |
| 450 | | - zHost = mprintf("cert:%s", |
| 451 | | - pUrlData->useProxy ? pUrlData->hostname : pUrlData->name); |
| 452 | | - zCert = db_get(zHost, NULL); |
| 453 | | - free(zHost); |
| 454 | | - if ( zCert==NULL ) |
| 455 | | - return NULL; |
| 456 | | - |
| 457 | | - if ( pTrusted!=0 ){ |
| 458 | | - zHost = mprintf("trusted:%s", |
| 459 | | - pUrlData->useProxy ? pUrlData->hostname : pUrlData->name); |
| 460 | | - *pTrusted = db_get_int(zHost, 0); |
| 461 | | - free(zHost); |
| 462 | | - } |
| 463 | | - |
| 464 | | - mem = BIO_new(BIO_s_mem()); |
| 465 | | - BIO_puts(mem, zCert); |
| 466 | | - cert = PEM_read_bio_X509(mem, NULL, 0, NULL); |
| 467 | | - free(zCert); |
| 468 | | - BIO_free(mem); |
| 469 | | - return cert; |
| 452 | +LOCAL void ssl_one_time_exception( |
| 453 | + UrlData *pUrlData, |
| 454 | + const char *zHash |
| 455 | +){ |
| 456 | + fossil_free(sException.zHost); |
| 457 | + sException.zHost = fossil_strdup(pUrlData->name); |
| 458 | + fossil_free(sException.zHash); |
| 459 | + sException.zHash = fossil_strdup(zHash); |
| 470 | 460 | } |
| 471 | 461 | |
| 472 | 462 | /* |
| 473 | 463 | ** Send content out over the SSL connection. |
| 474 | 464 | */ |
| | @@ -510,22 +500,110 @@ |
| 510 | 500 | } |
| 511 | 501 | |
| 512 | 502 | #endif /* FOSSIL_ENABLE_SSL */ |
| 513 | 503 | |
| 514 | 504 | /* |
| 515 | | -** COMMAND: test-ssl-trust-store |
| 505 | +** COMMAND: tls-config* |
| 506 | +** |
| 507 | +** Usage: %fossil tls-config [SUBCOMMAND] [OPTIONS...] [ARGS...] |
| 508 | +** |
| 509 | +** This command is used to view or modify the TLS (Transport Layer |
| 510 | +** Security) configuration for Fossil. TLS (formerly SSL) is the |
| 511 | +** encryption technology used for secure HTTPS transport. |
| 512 | +** |
| 513 | +** Sub-commands: |
| 514 | +** |
| 515 | +** show Show the TLS configuration |
| 516 | 516 | ** |
| 517 | | -** Show the file and directory where OpenSSL looks for certificates |
| 518 | | -** of trusted CAs. |
| 517 | +** remove-exception DOMAIN... Remove TLS cert exceptions |
| 518 | +** for the domains listed. Or if |
| 519 | +** the --all option is specified, |
| 520 | +** remove all TLS cert exceptions. |
| 519 | 521 | */ |
| 520 | | -void test_ssl_info(void){ |
| 522 | +void test_tlsconfig_info(void){ |
| 523 | + const char *zCmd; |
| 524 | + size_t nCmd; |
| 525 | + int nHit = 0; |
| 521 | 526 | #if !defined(FOSSIL_ENABLE_SSL) |
| 522 | | - fossil_print("SSL disabled in this build\n"); |
| 527 | + fossil_print("TLS disabled in this build\n"); |
| 523 | 528 | #else |
| 524 | | - fossil_print("file: %-14s %s\n", |
| 525 | | - X509_get_default_cert_file_env(), |
| 526 | | - X509_get_default_cert_file()); |
| 527 | | - fossil_print("dir: %-14s %s\n", |
| 528 | | - X509_get_default_cert_dir_env(), |
| 529 | | - X509_get_default_cert_dir()); |
| 529 | + db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0); |
| 530 | + db_open_config(1,0); |
| 531 | + zCmd = g.argc>=3 ? g.argv[2] : "show"; |
| 532 | + nCmd = strlen(zCmd); |
| 533 | + if( strncmp("show",zCmd,nCmd)==0 ){ |
| 534 | + const char *zName, *zValue; |
| 535 | + size_t nName; |
| 536 | + Stmt q; |
| 537 | + fossil_print("OpenSSL-version: %s\n", SSLeay_version(SSLEAY_VERSION)); |
| 538 | + fossil_print("OpenSSL-cert-file: %s\n", X509_get_default_cert_file()); |
| 539 | + fossil_print("OpenSSL-cert-dir: %s\n", X509_get_default_cert_dir()); |
| 540 | + zName = X509_get_default_cert_file_env(); |
| 541 | + zValue = fossil_getenv(zName); |
| 542 | + if( zValue==0 ) zValue = ""; |
| 543 | + nName = strlen(zName); |
| 544 | + fossil_print("%s:%.*s%s\n", zName, 19-nName, "", zValue); |
| 545 | + zName = X509_get_default_cert_dir_env(); |
| 546 | + zValue = fossil_getenv(zName); |
| 547 | + if( zValue==0 ) zValue = ""; |
| 548 | + nName = strlen(zName); |
| 549 | + fossil_print("%s:%.*s%s\n", zName, 19-nName, "", zValue); |
| 550 | + nHit++; |
| 551 | + fossil_print("ssl-ca-location: %s\n", db_get("ssl-ca-location","")); |
| 552 | + fossil_print("ssl-identity: %s\n", db_get("ssl-identity","")); |
| 553 | + db_prepare(&q, |
| 554 | + "SELECT name FROM global_config" |
| 555 | + " WHERE name GLOB 'cert:*'" |
| 556 | + "UNION ALL " |
| 557 | + "SELECT name FROM config" |
| 558 | + " WHERE name GLOB 'cert:*'" |
| 559 | + " ORDER BY name" |
| 560 | + ); |
| 561 | + while( db_step(&q)==SQLITE_ROW ){ |
| 562 | + fossil_print("exception: %s\n", db_column_text(&q,0)+5); |
| 563 | + } |
| 564 | + db_finalize(&q); |
| 565 | + }else |
| 566 | + if( strncmp("remove-exception",zCmd,nCmd)==0 ){ |
| 567 | + int i; |
| 568 | + Blob sql; |
| 569 | + char *zSep = "("; |
| 570 | + db_begin_transaction(); |
| 571 | + blob_init(&sql, 0, 0); |
| 572 | + if( g.argc==4 && find_option("all",0,0)!=0 ){ |
| 573 | + blob_append_sql(&sql, |
| 574 | + "DELETE FROM global_config WHERE name GLOB 'cert:*';\n" |
| 575 | + "DELETE FROM global_config WHERE name GLOB 'trusted:*';\n" |
| 576 | + "DELETE FROM config WHERE name GLOB 'cert:*';\n" |
| 577 | + "DELETE FROM config WHERE name GLOB 'trusted:*';\n" |
| 578 | + ); |
| 579 | + }else{ |
| 580 | + if( g.argc<4 ){ |
| 581 | + usage("remove-exception DOMAIN-NAME ..."); |
| 582 | + } |
| 583 | + blob_append_sql(&sql,"DELETE FROM global_config WHERE name IN "); |
| 584 | + for(i=3; i<g.argc; i++){ |
| 585 | + blob_append_sql(&sql,"%s'cert:%q','trust:%q'", |
| 586 | + zSep/*safe-for-%s*/, g.argv[i], g.argv[i]); |
| 587 | + zSep = ","; |
| 588 | + } |
| 589 | + blob_append_sql(&sql,");\n"); |
| 590 | + zSep = "("; |
| 591 | + blob_append_sql(&sql,"DELETE FROM config WHERE name IN "); |
| 592 | + for(i=3; i<g.argc; i++){ |
| 593 | + blob_append_sql(&sql,"%s'cert:%q','trusted:%q'", |
| 594 | + zSep/*safe-for-%s*/, g.argv[i], g.argv[i]); |
| 595 | + zSep = ","; |
| 596 | + } |
| 597 | + blob_append_sql(&sql,");"); |
| 598 | + } |
| 599 | + db_exec_sql(blob_str(&sql)); |
| 600 | + db_commit_transaction(); |
| 601 | + blob_reset(&sql); |
| 602 | + }else |
| 603 | + /*default*/{ |
| 604 | + fossil_fatal("unknown sub-command \"%s\".\nshould be one of:" |
| 605 | + " remove-exception show", |
| 606 | + zCmd); |
| 607 | + } |
| 530 | 608 | #endif |
| 531 | 609 | } |
| 532 | 610 | |