From 621c2b901527248b4822895bc0305373a7d2dd63 Mon Sep 17 00:00:00 2001 From: Daniel Stenberg Date: Fri, 6 Jun 2008 20:52:32 +0000 Subject: [PATCH] - Axel Tillequin and Arnaud Ebalard added support for CURLOPT_ISSUERCERT, for OpenSSL, NSS and GnuTLS-built libcurls. --- CHANGES | 3 ++ RELEASE-NOTES | 5 ++- TODO-RELEASE | 2 - docs/libcurl/curl_easy_setopt.3 | 16 ++++++++ docs/libcurl/libcurl-errors.3 | 2 + include/curl/curl.h | 5 +++ lib/gtls.c | 53 +++++++++++++++++++++++-- lib/nss.c | 69 ++++++++++++++++++++++++++++++++- lib/ssluse.c | 42 +++++++++++++++++++- lib/strerror.c | 3 ++ lib/url.c | 9 +++++ lib/urldata.h | 2 + 12 files changed, 199 insertions(+), 12 deletions(-) diff --git a/CHANGES b/CHANGES index 4145bbe0b9..5efd5df90b 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,9 @@ Daniel Stenberg (6 Jun 2008) +- Axel Tillequin and Arnaud Ebalard added support for CURLOPT_ISSUERCERT, for + OpenSSL, NSS and GnuTLS-built libcurls. + - Axel Tillequin and Arnaud Ebalard added support for CURLOPT_CRLFILE, for OpenSSL, NSS and GnuTLS-built libcurls. diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 9f7157e268..fc90d76113 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -2,7 +2,7 @@ Curl and libcurl 7.18.3 Public curl releases: 106 Command line options: 126 - curl_easy_setopt() options: 151 + curl_easy_setopt() options: 152 Public functions in libcurl: 58 Public web site mirrors: 37 Known libcurl bindings: 36 @@ -11,7 +11,8 @@ Curl and libcurl 7.18.3 This release includes the following changes: o Added CURLINFO_PRIMARY_IP - o Added CURLOPT_CRLFILE + o Added CURLOPT_CRLFILE and CURLE_SSL_CRL_BADFILE + o Added CURLOPT_ISSUERCERT and CURLE_SSL_ISSUER_ERROR This release includes the following bugfixes: diff --git a/TODO-RELEASE b/TODO-RELEASE index 661894c3dc..3fa2f0aff7 100644 --- a/TODO-RELEASE +++ b/TODO-RELEASE @@ -3,8 +3,6 @@ To be addressed before 7.18.3 (planned release: August 2008) 139 - Christopher Palow's CURLM_EASY_HANDLE_EXISTS patch -140 - Arnaud Ebalard and Axel Tillequin's CRL support and issuer check patches - 144 - Help apps use 64bit/LFS libcurl! 145 - diff --git a/docs/libcurl/curl_easy_setopt.3 b/docs/libcurl/curl_easy_setopt.3 index f1f086e84c..6e7ce3ec73 100644 --- a/docs/libcurl/curl_easy_setopt.3 +++ b/docs/libcurl/curl_easy_setopt.3 @@ -1443,6 +1443,22 @@ bundle is assumed to be stored, as established at build time. When built against NSS this is the directory that the NSS certificate database resides in. +.IP CURLOPT_ISSUERCERT +Pass a char * to a zero terminated string naming a file holding a CA +certificate in PEM format. If the option is set, an additional check against +the peer certificate is performed to verify the issuer is indeed the one +associated with the certificate provided by the option. This additional check +is useful in multi-level PKI where one need to enforce the peer certificate is +from a specific branch of the tree. + +This option makes sense only when used in combination with the +\fICURLOPT_SSL_VERIFYPEER\fP option. Otherwise, the result of the check is not +considered as failure. + +A specific error code (CURLE_SSL_ISSUER_ERROR) is defined with the option, +which is returned if the setup of the SSL/TLS session has failed due to a +mismatch with the issuer of peer certificate (\fICURLOPT_SSL_VERIFYPEER\fP has +to be set too for the check to fail). (Added in 7.18.3) .IP CURLOPT_CAPATH Pass a char * to a zero terminated string naming a directory holding multiple CA certificates to verify the peer with. The certificate directory must be diff --git a/docs/libcurl/libcurl-errors.3 b/docs/libcurl/libcurl-errors.3 index 994489b71f..c1ab312370 100644 --- a/docs/libcurl/libcurl-errors.3 +++ b/docs/libcurl/libcurl-errors.3 @@ -214,6 +214,8 @@ return code is only returned from \fIcurl_easy_recv(3)\fP and \fIcurl_easy_send(3)\fP (Added in 7.18.2) .IP "CURLE_SSL_CRL_BADFILE (82)" Failed to load CRL file (Added in 7.18.3) +.IP "CURLE_SSL_ISSUER_ERROR (83)" +Issuer check failed (Added in 7.18.3) .IP "CURLE_OBSOLETE*" These error codes will never be returned. They used to be used in an old libcurl version and are currently unused. diff --git a/include/curl/curl.h b/include/curl/curl.h index a67fd22103..556c2226a9 100644 --- a/include/curl/curl.h +++ b/include/curl/curl.h @@ -456,6 +456,8 @@ typedef enum { in 7.18.2) */ CURLE_SSL_CRL_BADFILE, /* 82 - could not load CRL file, missing or wrong format (Added in 7.18.3) */ + CURLE_SSL_ISSUER_ERROR, /* 83 - Issuer check failed. (Added in + 7.18.3) */ CURL_LAST /* never use! */ } CURLcode; @@ -1206,6 +1208,9 @@ typedef enum { /* CRL file */ CINIT(CRLFILE, OBJECTPOINT, 169), + /* Issuer certificate */ + CINIT(ISSUERCERT, OBJECTPOINT, 170), + CURLOPT_LASTENTRY /* the last unused */ } CURLoption; diff --git a/lib/gtls.c b/lib/gtls.c index e9e410243d..6e762f1616 100644 --- a/lib/gtls.c +++ b/lib/gtls.c @@ -143,6 +143,32 @@ static void showtime(struct SessionHandle *data, infof(data, "%s", data->state.buffer); } +static gnutls_datum load_file (const char *file) +{ + FILE *f; + gnutls_datum loaded_file = { NULL, 0 }; + long filelen; + void *ptr; + + if (!(f = fopen(file, "r")) + || fseek(f, 0, SEEK_END) != 0 + || (filelen = ftell(f)) < 0 + || fseek(f, 0, SEEK_SET) != 0 + || !(ptr = malloc((size_t)filelen)) + || fread(ptr, 1, (size_t)filelen, f) < (size_t)filelen) { + return loaded_file; + } + + loaded_file.data = ptr; + loaded_file.size = (unsigned int)filelen; + return loaded_file; +} + +static void unload_file(gnutls_datum data) { + free(data.data); +} + + /* this function does a BLOCKING SSL/TLS (re-)handshake */ static CURLcode handshake(struct connectdata *conn, gnutls_session session, @@ -221,7 +247,8 @@ Curl_gtls_connect(struct connectdata *conn, unsigned int cert_list_size; const gnutls_datum *chainp; unsigned int verify_status; - gnutls_x509_crt x509_cert; + gnutls_x509_crt x509_cert,x509_issuer; + gnutls_datum issuerp; char certbuf[256]; /* big enough? */ size_t size; unsigned int algo; @@ -375,7 +402,9 @@ Curl_gtls_connect(struct connectdata *conn, chainp = gnutls_certificate_get_peers(session, &cert_list_size); if(!chainp) { - if(data->set.ssl.verifypeer) { + if(data->set.ssl.verifypeer || + data->set.ssl.verifyhost || + data->set.ssl.issuercert) { failf(data, "failed to get server cert"); return CURLE_PEER_FAILED_VERIFICATION; } @@ -399,8 +428,9 @@ Curl_gtls_connect(struct connectdata *conn, /* verify_status is a bitmask of gnutls_certificate_status bits */ if(verify_status & GNUTLS_CERT_INVALID) { if(data->set.ssl.verifypeer) { - failf(data, "server certificate verification failed. CAfile: %s", - data->set.ssl.CAfile?data->set.ssl.CAfile:"none"); + failf(data, "server certificate verification failed. CAfile: %s " + "CRLfile: %s", data->set.ssl.CAfile?data->set.ssl.CAfile:"none", + data->set.ssl.CRLfile?data->set.ssl.CRLfile:"none"); return CURLE_SSL_CACERT; } else @@ -419,6 +449,21 @@ Curl_gtls_connect(struct connectdata *conn, gnutls_x509_crt_t format */ gnutls_x509_crt_import(x509_cert, chainp, GNUTLS_X509_FMT_DER); + if (data->set.ssl.issuercert) { + gnutls_x509_crt_init(&x509_issuer); + issuerp = load_file(data->set.ssl.issuercert); + gnutls_x509_crt_import(x509_issuer, &issuerp, GNUTLS_X509_FMT_PEM); + rc = gnutls_x509_crt_check_issuer(x509_cert,x509_issuer); + unload_file(issuerp); + if (rc <= 0) { + failf(data, "server certificate issuer check failed (IssuerCert: %s)", + data->set.ssl.issuercert?data->set.ssl.issuercert:"none"); + return CURLE_SSL_ISSUER_ERROR; + } + infof(data,"\t server certificate issuer check OK (Issuer Cert: %s)\n", + data->set.ssl.issuercert?data->set.ssl.issuercert:"none"); + } + size=sizeof(certbuf); rc = gnutls_x509_crt_get_dn_by_oid(x509_cert, GNUTLS_OID_X520_COMMON_NAME, 0, /* the first and only one */ diff --git a/lib/nss.c b/lib/nss.c index a5fc795f8a..5e01c4448e 100644 --- a/lib/nss.c +++ b/lib/nss.c @@ -721,6 +721,43 @@ static void display_conn_info(struct connectdata *conn, PRFileDesc *sock) return; } +/** + * + * Check that the Peer certificate's issuer certificate matches the one found + * by issuer_nickname. This is not exactly the way OpenSSL and GNU TLS do the + * issuer check, so we provide comments that mimic the OpenSSL + * X509_check_issued function (in x509v3/v3_purp.c) + */ +static SECStatus check_issuer_cert(struct connectdata *conn, PRFileDesc *sock, char* issuer_nickname) +{ + CERTCertificate *cert,*cert_issuer,*issuer; + SECStatus res=SECSuccess; + void *proto_win = NULL; + + /* + PRArenaPool *tmpArena = NULL; + CERTAuthKeyID *authorityKeyID = NULL; + SECITEM *caname = NULL; + */ + + cert = SSL_PeerCertificate(sock); + cert_issuer = CERT_FindCertIssuer(cert,PR_Now(),certUsageObjectSigner); + + proto_win = SSL_RevealPinArg(sock); + issuer = NULL; + issuer = PK11_FindCertFromNickname(issuer_nickname, proto_win); + + if ((!cert_issuer) || (!issuer)) + res = SECFailure; + else if (CERT_CompareCerts(cert_issuer,issuer)==PR_FALSE) + res = SECFailure; + + CERT_DestroyCertificate(cert); + CERT_DestroyCertificate(issuer); + CERT_DestroyCertificate(cert_issuer); + return res; +} + /** * * Callback to pick the SSL client certificate. @@ -853,7 +890,7 @@ int Curl_nss_close_all(struct SessionHandle *data) return 0; } -CURLcode Curl_nss_connect(struct connectdata * conn, int sockindex) +CURLcode Curl_nss_connect(struct connectdata *conn, int sockindex) { PRInt32 err; PRFileDesc *model = NULL; @@ -1046,6 +1083,7 @@ CURLcode Curl_nss_connect(struct connectdata * conn, int sockindex) } else { strncpy(nickname, data->set.str[STRING_CERT], PATH_MAX); + nickname[PATH_MAX-1]=0; /* make sure this is zero terminated */ } if(nss_Init_Tokens(conn) != SECSuccess) { free(nickname); @@ -1061,7 +1099,7 @@ CURLcode Curl_nss_connect(struct connectdata * conn, int sockindex) connssl->client_nickname = strdup(nickname); if(SSL_GetClientAuthDataHook(model, (SSLGetClientAuthData) SelectClientCert, - (void *)connssl->client_nickname) != + (void *)connssl) != SECSuccess) { curlerr = CURLE_SSL_CERTPROBLEM; goto error; @@ -1074,6 +1112,7 @@ CURLcode Curl_nss_connect(struct connectdata * conn, int sockindex) else connssl->client_nickname = NULL; + /* Import our model socket onto the existing file descriptor */ connssl->handle = PR_ImportTCPSocket(sockfd); connssl->handle = SSL_ImportFD(model, connssl->handle); @@ -1099,6 +1138,32 @@ CURLcode Curl_nss_connect(struct connectdata * conn, int sockindex) display_conn_info(conn, connssl->handle); + if (data->set.str[STRING_SSL_ISSUERCERT]) { + char *n; + char *nickname; + nickname = (char *)malloc(PATH_MAX); + if(is_file(data->set.str[STRING_SSL_ISSUERCERT])) { + n = strrchr(data->set.str[STRING_SSL_ISSUERCERT], '/'); + if (n) { + n++; /* skip last slash */ + snprintf(nickname, PATH_MAX, "PEM Token #%ld:%s", 1, n); + } + } + else { + strncpy(nickname, data->set.str[STRING_SSL_ISSUERCERT], PATH_MAX); + nickname[PATH_MAX-1]=0; /* make sure this is zero terminated */ + } + if (check_issuer_cert(conn,connssl->handle,nickname)==SECFailure) { + infof(data,"SSL certificate issuer check failed\n"); + free(nickname); + curlerr = CURLE_SSL_ISSUER_ERROR; + goto error; + } + else { + infof("SSL certificate issuer check ok\n"); + } + } + return CURLE_OK; error: diff --git a/lib/ssluse.c b/lib/ssluse.c index f14ad344e8..4fff13d78a 100644 --- a/lib/ssluse.c +++ b/lib/ssluse.c @@ -1629,6 +1629,9 @@ static CURLcode servercert(struct connectdata *conn, long lerr; ASN1_TIME *certdate; struct SessionHandle *data = conn->data; + X509 *issuer; + FILE *fp; + connssl->server_cert = SSL_get_peer_certificate(connssl->handle); if(!connssl->server_cert) { if(strict) @@ -1678,6 +1681,41 @@ static CURLcode servercert(struct connectdata *conn, /* We could do all sorts of certificate verification stuff here before deallocating the certificate. */ + /* e.g. match issuer name with provided issuer certificate */ + if (data->set.str[STRING_SSL_ISSUERCERT]) { + if (! (fp=fopen(data->set.str[STRING_SSL_ISSUERCERT],"r"))) { + if (strict) + failf(data, "SSL: Unable to open issuer cert (%s)\n", + data->set.str[STRING_SSL_ISSUERCERT]); + X509_free(connssl->server_cert); + connssl->server_cert = NULL; + return CURLE_SSL_ISSUER_ERROR; + } + issuer = PEM_read_X509(fp,NULL,NULL,NULL); + if (!issuer) { + if (strict) + failf(data, "SSL: Unable to read issuer cert (%s)\n", + data->set.str[STRING_SSL_ISSUERCERT]); + X509_free(connssl->server_cert); + X509_free(issuer); + fclose(fp); + return CURLE_SSL_ISSUER_ERROR; + } + fclose(fp); + if (X509_check_issued(issuer,connssl->server_cert) != X509_V_OK) { + if (strict) + failf(data, "SSL: Certificate issuer check failed (%s)\n", + data->set.str[STRING_SSL_ISSUERCERT]); + X509_free(connssl->server_cert); + X509_free(issuer); + connssl->server_cert = NULL; + return CURLE_SSL_ISSUER_ERROR; + } + infof(data, "\t SSL certificate issuer check ok (%s)\n", + data->set.str[STRING_SSL_ISSUERCERT]); + X509_free(issuer); + } + lerr = data->set.ssl.certverifyresult= SSL_get_verify_result(connssl->handle); if(data->set.ssl.certverifyresult != X509_V_OK) { @@ -1690,12 +1728,12 @@ static CURLcode servercert(struct connectdata *conn, retcode = CURLE_PEER_FAILED_VERIFICATION; } else - infof(data, "SSL certificate verify result: %s (%ld)," + infof(data, "\t SSL certificate verify result: %s (%ld)," " continuing anyway.\n", X509_verify_cert_error_string(lerr), lerr); } else - infof(data, "SSL certificate verify ok.\n"); + infof(data, "\t SSL certificate verify ok.\n"); } X509_free(connssl->server_cert); diff --git a/lib/strerror.c b/lib/strerror.c index 03b01582c5..032e2c7476 100644 --- a/lib/strerror.c +++ b/lib/strerror.c @@ -225,6 +225,9 @@ curl_easy_strerror(CURLcode error) case CURLE_SSL_CRL_BADFILE: return "Failed to load CRL file (path? access rights?, format?)"; + case CURLE_SSL_ISSUER_ERROR: + return "Issuer check against peer certificate failed"; + case CURLE_SEND_FAIL_REWIND: return "Send failed since rewinding of the data stream failed"; diff --git a/lib/url.c b/lib/url.c index 0e71cede0d..a7f1a0c1a8 100644 --- a/lib/url.c +++ b/lib/url.c @@ -1819,6 +1819,14 @@ CURLcode Curl_setopt(struct SessionHandle *data, CURLoption option, result = setstropt(&data->set.str[STRING_SSL_CRLFILE], va_arg(param, char *)); break; + case CURLOPT_ISSUERCERT: + /* + * Set Issuer certificate file + * to check certificates issuer + */ + result = setstropt(&data->set.str[STRING_SSL_ISSUERCERT], + va_arg(param, char *)); + break; case CURLOPT_TELNETOPTIONS: /* * Set a linked list of telnet options @@ -3960,6 +3968,7 @@ static CURLcode CreateConnection(struct SessionHandle *data, data->set.ssl.CApath = data->set.str[STRING_SSL_CAPATH]; data->set.ssl.CAfile = data->set.str[STRING_SSL_CAFILE]; data->set.ssl.CRLfile = data->set.str[STRING_SSL_CRLFILE]; + data->set.ssl.issuercert = data->set.str[STRING_SSL_ISSUERCERT]; data->set.ssl.random_file = data->set.str[STRING_SSL_RANDOM_FILE]; data->set.ssl.egdsocket = data->set.str[STRING_SSL_EGDSOCKET]; data->set.ssl.cipher_list = data->set.str[STRING_SSL_CIPHER_LIST]; diff --git a/lib/urldata.h b/lib/urldata.h index def598b41f..2e0f692045 100644 --- a/lib/urldata.h +++ b/lib/urldata.h @@ -213,6 +213,7 @@ struct ssl_config_data { char *CApath; /* certificate dir (doesn't work on windows) */ char *CAfile; /* cerficate to verify peer against */ char *CRLfile; /* CRL to check cerficate revocation */ + char *issuercert; /* optional issuer cerficate filename */ char *random_file; /* path to file containing "random" data */ char *egdsocket; /* path to file containing the EGD daemon socket */ char *cipher_list; /* list of ciphers to use */ @@ -1319,6 +1320,7 @@ enum dupstring { STRING_USERPWD, /* , if used */ STRING_SSH_HOST_PUBLIC_KEY_MD5, /* md5 of host public key in ascii hex */ STRING_SSL_CRLFILE, /* crl file to check certificate */ + STRING_SSL_ISSUERCERT, /* issuer cert file to check certificate */ /* -- end of strings -- */ STRING_LAST /* not used, just an end-of-list marker */