Index: subversion/include/svn_error_codes.h =================================================================== --- subversion/include/svn_error_codes.h (revision 11465) +++ subversion/include/svn_error_codes.h (working copy) @@ -642,6 +642,17 @@ SVN_ERR_RA_SVN_CATEGORY_START + 6, "Client/server version mismatch") + /* @since New in 1.2. */ + SVN_ERRDEF (SVN_ERR_RA_SVN_SSL_INIT, + SVN_ERR_RA_SVN_CATEGORY_START + 7, + "RA layer failed to init SSL") + + /* @since New in 1.2. */ + SVN_ERRDEF (SVN_ERR_RA_SVN_SSL_ERROR, + SVN_ERR_RA_SVN_CATEGORY_START + 8, + "SSL network error") + + /* libsvn_auth errors */ /* this error can be used when an auth provider doesn't have Index: subversion/include/svn_base64.h =================================================================== --- subversion/include/svn_base64.h (revision 11465) +++ subversion/include/svn_base64.h (working copy) @@ -61,6 +61,15 @@ apr_pool_t *pool); +/** Returns a base64 encoding of @c buffer. + * + * Allocate the returned encoding in @c pool. + */ +svn_stringbuf_t *svn_base64_from_buffer (unsigned char buffer[], + apr_size_t len, + apr_pool_t *pool); + + /** Return a base64-encoded checksum for finalized @c digest. * * @c digest contains @c APR_MD5_DIGESTSIZE bytes of finalized data. Index: subversion/include/svn_ra_svn.h =================================================================== --- subversion/include/svn_ra_svn.h (revision 11465) +++ subversion/include/svn_ra_svn.h (working copy) @@ -41,6 +41,7 @@ /** Currently-defined capabilities. */ #define SVN_RA_SVN_CAP_EDIT_PIPELINE "edit-pipeline" +#define SVN_RA_SVN_CAP_SSL "ssl" /** A value used to indicate an optional number element in a tuple that was * not received. @@ -334,6 +335,27 @@ */ const svn_version_t *svn_ra_svn_version (void); +/** + * Initializes SSL session for the connection from @a ssl_ctx. + * @since New in 1.2. + */ +svn_error_t *svn_ra_svn_ssl_init(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + void *ssl_ctx); + +/** This function is only intended for use by svnserve. + * + * Wait for a SSL connection. Assumes that svn_ra_svn_ssl_init has been called. + * @since New in 1.2. + */ +svn_error_t *svn_ra_svn_ssl_accept(svn_ra_svn_conn_t *conn, apr_pool_t *pool); + +/** This function is only intended for use by client. + * + * Initiate a SSL connection. Assumes that svn_ra_svn_ssl_init has been called. + * @since New in 1.2. + */ +svn_error_t *svn_ra_svn_ssl_connect(svn_ra_svn_conn_t *conn, apr_pool_t *pool); + #ifdef __cplusplus } #endif /* __cplusplus */ Index: subversion/libsvn_subr/svn_base64.c =================================================================== --- subversion/libsvn_subr/svn_base64.c (revision 11465) +++ subversion/libsvn_subr/svn_base64.c (working copy) @@ -336,27 +336,34 @@ svn_stringbuf_t * -svn_base64_from_md5 (unsigned char digest[], apr_pool_t *pool) +svn_base64_from_buffer (unsigned char digest[], apr_size_t len, apr_pool_t *pool) { - svn_stringbuf_t *md5str; + svn_stringbuf_t *str; unsigned char ingroup[3]; int ingrouplen = 0, linelen = 0; - md5str = svn_stringbuf_create ("", pool); + str = svn_stringbuf_create ("", pool); /* This cast is safe because we know encode_bytes does a memcpy and * does an implicit unsigned char * cast. */ - encode_bytes (md5str, (char*)digest, APR_MD5_DIGESTSIZE, ingroup, + encode_bytes (str, (char*)digest, len, ingroup, &ingrouplen, &linelen); - encode_partial_group (md5str, ingroup, ingrouplen, linelen); + encode_partial_group (str, ingroup, ingrouplen, linelen); /* Our base64-encoding routines append a final newline if any data was created at all, so let's hack that off. */ - if ((md5str)->len) + if ((str)->len) { - (md5str)->len--; - (md5str)->data[(md5str)->len] = 0; + (str)->len--; + (str)->data[(str)->len] = 0; } - return md5str; + return str; } + + +svn_stringbuf_t * +svn_base64_from_md5 (unsigned char digest[], apr_pool_t *pool) +{ + return svn_base64_from_buffer(digest, APR_MD5_DIGESTSIZE, pool); +} Index: subversion/libsvn_ra_svn/client.c =================================================================== --- subversion/libsvn_ra_svn/client.c (revision 11465) +++ subversion/libsvn_ra_svn/client.c (working copy) @@ -37,16 +37,21 @@ #include "svn_ra.h" #include "svn_ra_svn.h" #include "svn_md5.h" +#include "svn_base64.h" #include "ra_svn.h" +#include + typedef struct { svn_ra_svn_conn_t *conn; int protocol_version; svn_boolean_t is_tunneled; svn_auth_baton_t *auth_baton; const char *user; + const char *hostname; const char *realm_prefix; + SSL_CTX *ssl_ctx; } ra_svn_session_baton_t; typedef struct { @@ -245,9 +250,10 @@ svn_boolean_t compat) { if (compat) - return svn_ra_svn_write_tuple(conn, pool, "nw(?c)(w)", (apr_uint64_t) 1, + return svn_ra_svn_write_tuple(conn, pool, "nw(?c)(ww)", (apr_uint64_t) 1, mech, mech_arg, - SVN_RA_SVN_CAP_EDIT_PIPELINE); + SVN_RA_SVN_CAP_EDIT_PIPELINE, + SVN_RA_SVN_CAP_SSL); else return svn_ra_svn_write_tuple(conn, pool, "w(?c)", mech, mech_arg); } @@ -267,6 +273,273 @@ return SVN_NO_ERROR; } +/* Format an ASN1 time to a string. + * Adapted from Neon library. + */ +static svn_boolean_t asn1time_to_string(ASN1_TIME *tm, char *buffer, + apr_size_t len) +{ + int num_read; + svn_boolean_t OK = FALSE; + BIO *bio = BIO_new(BIO_s_mem()); + if (bio) + { + if (ASN1_TIME_print(bio, tm) && len > 1) + { + num_read = BIO_read(bio, buffer, len-1); + if (num_read > 1) + { + buffer[num_read] = '\0'; + OK = TRUE; + } + else + OK = FALSE; + } + BIO_free(bio); + } + return OK; +} + +/* Compare peername against hostname. Allow wildcard in leftmost + * position in peername, and the comparision is case insensitive. + */ +static svn_boolean_t match_hostname(const char *peername, + const char *hostname, apr_pool_t *pool) +{ + char *str, *last_str; + char *hostname_copy; + + if (apr_strnatcasecmp(peername, hostname) == 0) + return TRUE; + + if (strlen(peername) < 3) + return FALSE; + + if (peername[0] != '*' || peername[1] != '.') + return FALSE; + + hostname_copy = apr_pstrdup(pool, hostname); + str = apr_strtok (hostname_copy, ".", &last_str); + if (str == NULL) + return FALSE; + + return (apr_strnatcasecmp(&peername[2], str) == 0); +} + +/* Verify that the certificates common name matches the hostname. + * Adapted from verify_callback() in postfixtls patch by Lutz Jaenicke + * at http://www.aet.tu-cottbus.de/personen/jaenicke/postfix_tls/ + */ +static svn_boolean_t verify_hostname(ra_svn_session_baton_t *sess, + apr_pool_t *pool, + svn_auth_ssl_server_cert_info_t *cert_info) +{ + int i, r; + svn_boolean_t matched = FALSE; + svn_boolean_t dnsname_found = FALSE; + X509 *peer = SSL_get_peer_certificate(sess->conn->ssl); + STACK_OF(GENERAL_NAME) *gens; + + /* + * Check out the name certified against the hostname expected. + * Standards are not always clear with respect to the handling of + * dNSNames. RFC3207 does not specify the handling. We therefore follow + * the strict rules in RFC2818 (HTTP over TLS), Section 3.1: + * The Subject Alternative Name/dNSName has precedence over CommonName + * (CN). If dNSName entries are provided, CN is not checked anymore. + */ + + gens = X509_get_ext_d2i(peer, NID_subject_alt_name, 0, 0); + if (gens) + { + for (i = 0, r = sk_GENERAL_NAME_num(gens); i < r; i++) + { + const GENERAL_NAME *gn = sk_GENERAL_NAME_value(gens, i); + if (gn->type == GEN_DNS) + { + dnsname_found = TRUE; + matched = match_hostname(gn->d.ia5->data, sess->hostname, pool); + if (matched) + break; + } + } + sk_GENERAL_NAME_free(gens); + if (dnsname_found) + return matched; + } + + return match_hostname(cert_info->hostname, sess->hostname, pool); +} + +/* Fill in the server certificate information, as well as check for some + * errors in the certificate. + */ +static svn_error_t *fill_server_cert_info(ra_svn_session_baton_t *sess, + apr_pool_t *pool, + svn_auth_ssl_server_cert_info_t *cert_info, + apr_uint32_t *cert_failures) +{ + #define CERT_BUFFER_SIZE 256 + const char hexcodes[] = "0123456789ABCDEF"; + X509 *peer; + char buffer[CERT_BUFFER_SIZE]; + unsigned char md[EVP_MAX_MD_SIZE]; + char fingerprint[EVP_MAX_MD_SIZE * 3]; + unsigned int md_size; + unsigned int i; + unsigned int cert_buffer_size; + unsigned char *cert_buffer, *cert_buffer_pointer; + svn_stringbuf_t *ascii_cert; + long verify_result; + + cert_info->hostname = NULL; + cert_info->fingerprint = NULL; + cert_info->valid_from = NULL; + cert_info->valid_until = NULL; + cert_info->issuer_dname = NULL; + cert_info->ascii_cert = NULL; + + *cert_failures = 0; + + peer = SSL_get_peer_certificate(sess->conn->ssl); + if (peer == NULL) + return svn_error_create(SVN_ERR_RA_SVN_SSL_ERROR, NULL, + _("Unable to obtain server certificate")); + + + if (!X509_NAME_get_text_by_NID(X509_get_subject_name(peer), + NID_commonName, buffer, CERT_BUFFER_SIZE)) + return svn_error_create(SVN_ERR_RA_SVN_SSL_ERROR, NULL, + _("Could not obtain server certificate CN")); + cert_info->hostname = apr_pstrdup(pool, buffer); + + if (!X509_NAME_get_text_by_NID(X509_get_issuer_name(peer), + NID_commonName, buffer, CERT_BUFFER_SIZE)) + { + if (!X509_NAME_get_text_by_NID(X509_get_issuer_name(peer), + NID_organizationName, buffer, + CERT_BUFFER_SIZE)) + return svn_error_create(SVN_ERR_RA_SVN_SSL_ERROR, NULL, + _("Could not obtain server certificate issuer " + "or organization")); + + } + cert_info->issuer_dname = apr_pstrdup(pool, buffer); + + /* Neon uses sha1 for calculating fingerprint, and not MD5. */ + if (X509_digest(peer, EVP_sha1(), md, &md_size)) + { + for (i=0; i> 4]; + fingerprint[(3*i)+1] = hexcodes[(md[i] & 0x0f)]; + fingerprint[(3*i)+2] = ':'; + } + if (md_size > 0) + fingerprint[(3*(md_size-1))+2] = '\0'; + else + fingerprint[0] = '\0'; + cert_info->fingerprint = apr_pstrdup(pool, fingerprint); + } + else + cert_info->fingerprint = apr_pstrdup(pool, ""); + + cert_buffer_size = i2d_X509(peer, NULL); + if (cert_buffer_size > 0) + { + cert_buffer = apr_pcalloc(pool, cert_buffer_size); + cert_buffer_pointer = cert_buffer; + + /* cert_buffer_pointer is automatically incrementeded. */ + i2d_X509(peer, &cert_buffer_pointer); + + ascii_cert = svn_base64_from_buffer(cert_buffer, cert_buffer_size, pool); + cert_info->ascii_cert = apr_pstrdup(pool, ascii_cert->data); + } + + /* Read the certificate validity dates, but keep the output format + * same as in Neon. */ + if (asn1time_to_string(X509_get_notBefore(peer), buffer, CERT_BUFFER_SIZE)) + cert_info->valid_from = apr_pstrdup(pool, buffer); + else + cert_info->valid_from = apr_pstrdup(pool, "[invalid date]"); + + if (asn1time_to_string(X509_get_notAfter(peer), buffer, CERT_BUFFER_SIZE)) + cert_info->valid_until = apr_pstrdup(pool, buffer); + else + cert_info->valid_until = apr_pstrdup(pool, "[invalid date]"); + + /* Now we start checking the certificate. Similarly as done in Neon.*/ + if (X509_cmp_current_time(X509_get_notBefore(peer)) >= 0) + *cert_failures |= SVN_AUTH_SSL_NOTYETVALID; + else if (X509_cmp_current_time(X509_get_notAfter(peer)) <= 0) + *cert_failures |= SVN_AUTH_SSL_EXPIRED; + + /* Only the last verification failure will be returned from + * SSL_get_verify_result, even thugh there may be several errors. */ + verify_result = SSL_get_verify_result(sess->conn->ssl); + switch (verify_result) + { + case X509_V_OK: + case X509_V_ERR_CERT_NOT_YET_VALID: + case X509_V_ERR_CERT_HAS_EXPIRED: + break; + case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY: + case X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN: + case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT: + *cert_failures |= SVN_AUTH_SSL_UNKNOWNCA; + break; + default: + *cert_failures |= SVN_AUTH_SSL_OTHER; + } + + if (!verify_hostname(sess, pool, cert_info)) + *cert_failures |= SVN_AUTH_SSL_CNMISMATCH; + + return SVN_NO_ERROR; +} + +/* Authenticate the server certificate. + */ +static svn_error_t *do_ssl_auth(ra_svn_session_baton_t *sess, + apr_pool_t *pool) +{ + svn_auth_iterstate_t *state; + void *creds; + svn_error_t *error; + apr_uint32_t *cert_failures = apr_palloc (pool, sizeof(*cert_failures)); + + svn_auth_ssl_server_cert_info_t *cert_info = + apr_palloc(pool, sizeof(*cert_info)); + + SVN_ERR(fill_server_cert_info(sess, pool, cert_info, cert_failures)); + + svn_auth_set_parameter(sess->auth_baton, + SVN_AUTH_PARAM_SSL_SERVER_FAILURES, + cert_failures); + + svn_auth_set_parameter(sess->auth_baton, + SVN_AUTH_PARAM_SSL_SERVER_CERT_INFO, + cert_info); + + SVN_ERR(svn_auth_first_credentials(&creds, &state, + SVN_AUTH_CRED_SSL_SERVER_TRUST, + sess->realm_prefix, + sess->auth_baton, + pool)); + + if (!creds) + return svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL, + _("Server certificate rejected")); + + error = svn_auth_save_credentials(state, pool); + + svn_auth_set_parameter(sess->auth_baton, + SVN_AUTH_PARAM_SSL_SERVER_CERT_INFO, NULL); + + return error; +} + /* Respond to an auth request and perform authentication. REALM may * be NULL for the initial authentication exchange of protocol version * 1. */ @@ -556,6 +829,46 @@ return SVN_NO_ERROR; } +/* Initializes the SSL context to be used by the client. */ +static svn_error_t *init_ssl_ctx(ra_svn_session_baton_t *sess, + apr_hash_t *config, + apr_pool_t *pool) +{ + /* List of ciphers that we allow for SSL connections. */ + const char *cipher_list = "ALL:!LOW"; + + SSL_load_error_strings(); + SSL_library_init(); + + /* TODO : Seed the randum number generator (RNG) + * for those operating systems that does not have /dev/urandom. + */ + + sess->ssl_ctx = SSL_CTX_new(SSLv23_client_method()); + if (sess->ssl_ctx == NULL) + return svn_error_create(SVN_ERR_RA_SVN_SSL_INIT, NULL, + _("No SSL context created")); + + if (SSL_CTX_set_cipher_list(sess->ssl_ctx, cipher_list) != 1) + return svn_error_create(SVN_ERR_RA_SVN_SSL_INIT, NULL, + _("Could not set cipher list for SSL")); + + return SVN_NO_ERROR; +} + +/* Frees the allocated SSL context. + * Assumes that this is called after freeing of SSL + * allocated by svn_ra_svn_create_conn. */ +static apr_status_t destroy_ssl_ctx(void *data) +{ + ra_svn_session_baton_t *sess = data; + + if (sess->ssl_ctx != NULL) + SSL_CTX_free(sess->ssl_ctx); + + return APR_SUCCESS; +} + static svn_error_t *ra_svn_open(void **baton, const char *url, const svn_ra_callbacks_t *callbacks, void *callback_baton, @@ -601,14 +914,17 @@ sess->is_tunneled = (tunnel != NULL); sess->auth_baton = callbacks->auth_baton; sess->user = user; + sess->hostname = hostname; sess->realm_prefix = apr_psprintf(pool, "", hostname, port); + sess->ssl_ctx = NULL; /* In protocol version 2, we send back our protocol version, our * capability list, and the URL, and subsequently there is an auth * request. In version 1, we send back the protocol version, auth * mechanism, mechanism initial response, and capability list, and; * then send the URL after authentication. do_auth temporarily has - * support for the mixed-style response. */ + * support for the mixed-style response. + * Note : SSL is not supported for version 1. */ /* When we punt support for protocol version 1, we should: * - Eliminate this conditional and the similar one below * - Remove v1 support from auth_response and inline it into do_auth @@ -622,13 +938,37 @@ } else { - SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "n(w)c", (apr_uint64_t) 2, - SVN_RA_SVN_CAP_EDIT_PIPELINE, url)); + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "n(ww)c", (apr_uint64_t) 2, + SVN_RA_SVN_CAP_EDIT_PIPELINE, + SVN_RA_SVN_CAP_SSL, url)); + + /* Setup the SSL if it's supported by the server. */ + if (svn_ra_svn_has_capability(conn, SVN_RA_SVN_CAP_SSL)) + { + if (tunnel) + return svn_error_create(SVN_ERR_RA_NOT_IMPLEMENTED, NULL, + _("SSL is not implemented for tunnel")); + + /* Flush write buffer before initiating SSL handshake. */ + svn_ra_svn_flush(sess->conn, pool); + + /* Needs to cleanup SSL_CTX at destruction of the session. + * Note that SSL must be released before SSL_CTX. We cleanup + * SSL by registering a cleanup function in svn_ra_svn_ssl_init. */ + apr_pool_cleanup_register(pool, sess, destroy_ssl_ctx, + apr_pool_cleanup_null); + + SVN_ERR(init_ssl_ctx(sess, config, pool)); + SVN_ERR(svn_ra_svn_ssl_init(sess->conn, pool, sess->ssl_ctx)); + SVN_ERR(svn_ra_svn_ssl_connect(sess->conn, pool)); + SVN_ERR(do_ssl_auth(sess, pool)); + } + SVN_ERR(handle_auth_request(sess, pool)); } /* This is where the security layer would go into effect if we - * supported security layers, which is a ways off. */ + * supported security layers, which is a ways off. */ /* Read the repository's uuid and root URL. */ SVN_ERR(svn_ra_svn_read_cmd_response(conn, pool, "c?c", &conn->uuid, Index: subversion/libsvn_ra_svn/marshal.c =================================================================== --- subversion/libsvn_ra_svn/marshal.c (revision 11465) +++ subversion/libsvn_ra_svn/marshal.c (working copy) @@ -40,6 +40,15 @@ #define svn_iswhitespace(c) ((c) == ' ' || (c) == '\n') +/* --- SSL FORWARD DECLARATION --- */ + +/* Defined below. */ +static svn_error_t *do_ssl_operation(svn_ra_svn_conn_t *conn, + int (*hsfunc)(SSL *), + int (*rfunc)(SSL *, void *, int), + int (*wfunc)(SSL *, const void *, int), + char *buffer, int *size); + /* --- CONNECTION INITIALIZATION --- */ svn_ra_svn_conn_t *svn_ra_svn_create_conn(apr_socket_t *sock, @@ -60,6 +69,11 @@ conn->block_baton = NULL; conn->capabilities = apr_hash_make(pool); conn->pool = pool; + conn->use_ssl = FALSE; + conn->ssl = NULL; + conn->internal_bio = NULL; + conn->network_bio = NULL; + return conn; } @@ -108,7 +122,16 @@ { apr_pollfd_t pfd; int n; + svn_error_t *err; + if (conn->use_ssl) + { + /* Note thak SSL_pending may return number of bytes to read, + * even if the data is not application data. */ + err = do_ssl_operation(conn, SSL_pending, NULL, NULL, NULL, &n); + return (err || n <= 0) ? FALSE : TRUE; + } + if (conn->sock) { pfd.desc_type = APR_POLL_SOCKET; @@ -140,6 +163,29 @@ return data + copylen; } +/* Write data to SSL. */ +static svn_error_t *writebuf_output_ssl(svn_ra_svn_conn_t *conn, + apr_pool_t *pool, const char *data, + apr_size_t len) +{ + svn_error_t *err; + apr_pool_t *subpool = NULL; + + err = do_ssl_operation(conn, NULL, NULL, SSL_write, (char *) data, &len); + if (err) + return err; + + subpool = svn_pool_create(pool); + if (conn->block_handler != NULL) + { + subpool = svn_pool_create(pool); + err = conn->block_handler(conn, subpool, conn->block_baton); + apr_pool_destroy(subpool); + } + + return err; +} + /* Write data to socket or output file as appropriate. */ static svn_error_t *writebuf_output(svn_ra_svn_conn_t *conn, apr_pool_t *pool, const char *data, apr_size_t len) @@ -149,6 +195,9 @@ apr_size_t count; apr_pool_t *subpool = NULL; + if (conn->use_ssl) + return writebuf_output_ssl(conn, pool, data, len); + while (data < end) { count = end - data; @@ -231,12 +280,35 @@ return data + copylen; } +/* Read data from SSL. */ +static svn_error_t *readbuf_input_ssl(svn_ra_svn_conn_t *conn, char *data, + apr_size_t *len) +{ + svn_error_t *err; + + if (conn->block_handler) + apr_socket_timeout_set(conn->sock, -1); + err = do_ssl_operation(conn, NULL, SSL_read, NULL, data, len); + if (conn->block_handler) + apr_socket_timeout_set(conn->sock, -1); + if (err) + return err; + if (*len == 0) + return svn_error_create(SVN_ERR_RA_SVN_CONNECTION_CLOSED, NULL, + _("Connection closed unexpectedly")); + + return SVN_NO_ERROR; +} + /* Read data from socket or input file as appropriate. */ static svn_error_t *readbuf_input(svn_ra_svn_conn_t *conn, char *data, apr_size_t *len) { apr_status_t status; + if (conn->use_ssl) + return readbuf_input_ssl(conn, data, len); + /* Always block for reading. */ if (conn->sock && conn->block_handler) apr_socket_timeout_set(conn->sock, -1); @@ -884,3 +956,230 @@ SVN_ERR(svn_ra_svn_end_list(conn, pool)); return SVN_NO_ERROR; } + + +/* --- SSL FUNCTIONS --- */ + +/* + * The interface layer between network and BIO-pair. The BIO-pair buffers + * the data to/from the TLS layer. Hence, at any time, there may be data + * in the buffer that must be written to the network. This writing has + * highest priority because the handshake might fail otherwise. + * Only then a read_request can be satisfied. + * Adapted from network_biopair_interop() in postfixtls patch by Lutz Jaenicke + * at http://www.aet.tu-cottbus.de/personen/jaenicke/postfix_tls/ + */ +static svn_error_t *network_biopair_interop(svn_ra_svn_conn_t *conn) +{ + int want_write; + int num_write; + int write_pos; + int from_bio; + int want_read; + int num_read; + int to_bio; + apr_status_t status; + #define NETLAYER_BUFFERSIZE 8192 + char buffer[NETLAYER_BUFFERSIZE]; + + while ((want_write = BIO_ctrl_pending(conn->network_bio)) > 0) + { + if (want_write > NETLAYER_BUFFERSIZE) + want_write = NETLAYER_BUFFERSIZE; + from_bio = BIO_read(conn->network_bio, buffer, want_write); + + /* + * Write the complete contents of the buffer. Since TLS performs + * underlying handshaking, we cannot afford to leave the buffer + * unflushed, as we could run into a deadlock trap (the peer + * waiting for a final byte and we already waiting for his reply + * in read position). + */ + + write_pos = 0; + do { + num_write = from_bio - write_pos; + status = apr_socket_send(conn->sock, buffer + write_pos, &num_write); + if (status) + return svn_error_wrap_apr(status, _("Can't write to connection")); + write_pos += num_write; + } while (write_pos < from_bio); + } + + while ((want_read = BIO_ctrl_get_read_request(conn->network_bio)) > 0) + { + if (want_read > NETLAYER_BUFFERSIZE) + want_read = NETLAYER_BUFFERSIZE; + + num_read = want_read; + status = apr_socket_recv(conn->sock, buffer, &num_read); + if (status && !APR_STATUS_IS_EOF(status)) + return svn_error_wrap_apr(status, _("Can't read from connection")); + if (num_read == 0) + return svn_error_create(SVN_ERR_RA_SVN_CONNECTION_CLOSED, NULL, + _("Connection closed unexpectedly")); + + to_bio = BIO_write(conn->network_bio, buffer, num_read); + if (to_bio != num_read) + return svn_error_create(SVN_ERR_RA_SVN_SSL_ERROR, NULL, + _("Failed to read from SSL socket layer")); + } + + return SVN_NO_ERROR; +} + +/* + * Function to perform the handshake for SSL_accept(), SSL_connect(), + * and SSL_shutdown() and perform the SSL_read(), SSL_write() operations. + * Call the underlying network_biopair_interop-layer to make sure the + * write buffer is flushed after every operation (that did not fail with + * a fatal error). + * The value of *count will be the result of the underlying SSL_operation(), + * and for SSL_read/SSL_write this will be number of bytes read/written + * if the operation was successfull. + * + * Adapted from do_tls_operation() in postfixtls patch by Lutz Jaenicke + * at http://www.aet.tu-cottbus.de/personen/jaenicke/postfix_tls/ + */ +static svn_error_t *do_ssl_operation(svn_ra_svn_conn_t *conn, + int (*hsfunc)(SSL *), + int (*rfunc)(SSL *, void *, int), + int (*wfunc)(SSL *, const void *, int), + char *buffer, int *count) +{ + int status; + int ssl_err; + int count_val; + int ret_status = -1; + svn_boolean_t done = FALSE; + + count_val = (count == NULL) ? 0 : *count; + + while (!done) + { + if (hsfunc) + status = hsfunc(conn->ssl); + else if (rfunc) + status = rfunc(conn->ssl, buffer, count_val); + else + status = wfunc(conn->ssl, (const char *)buffer, count_val); + ssl_err = SSL_get_error(conn->ssl, status); + + switch (ssl_err) + { + case SSL_ERROR_NONE: /* success */ + ret_status = status; + done = TRUE; /* no break, flush buffer before */ + /* leaving */ + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_READ: + SVN_ERR(network_biopair_interop(conn)); + break; + case SSL_ERROR_ZERO_RETURN: /* connection was closed cleanly */ + case SSL_ERROR_SYSCALL: + case SSL_ERROR_SSL: + default: + ret_status = status; + done = TRUE; + }; + }; + + if (count != NULL) + *count = ret_status; + + if (ret_status > 0) + return SVN_NO_ERROR; + else + return svn_error_create(SVN_ERR_RA_SVN_SSL_ERROR, NULL, + _("SSL network problem")); +} + +/* Releases the resources allocated by SSL. */ +static apr_status_t cleanup_ssl(void *data) +{ + svn_ra_svn_conn_t *conn = (svn_ra_svn_conn_t *) data; + + if (conn->ssl == NULL) + return APR_SUCCESS;; + + /* The connection has been setup between client and server, + so we tell the other side that we are finished. */ + if (conn->use_ssl) + { + if (!do_ssl_operation(conn, SSL_shutdown, NULL, NULL, NULL, NULL)) + do_ssl_operation(conn, SSL_shutdown, NULL, NULL, NULL, NULL); + conn->use_ssl = FALSE; + } + + /* Implicitely frees conn->internal_bio. */ + SSL_free(conn->ssl); + conn->ssl = NULL; + conn->internal_bio = NULL; + + if (conn->network_bio) + { + BIO_free(conn->network_bio); + conn->network_bio = NULL; + } + + return APR_SUCCESS; +} + +/* Creates and initializes an SSL that is to be used for this connection. + * Internally, a BIO pair will be used to transfer data to/from Subversion + * and the network. + * + * Subversion | TLS-engine + * | | + * +----------> SSL_operations() + * | /\ || + * | || \/ + * | BIO-pair (internal_bio) + * +----------< BIO-pair (network_bio) + * | | + * socket | + * + */ +svn_error_t *svn_ra_svn_ssl_init(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + void *ssl_ctx) +{ + SSL_CTX *ssl_context = (SSL_CTX *) ssl_ctx; + + /* Need to release SSL resources when the connection is destroyed. + * Assumes that the owning SSL_CTX is destroyed after cleanup + * of SSL.*/ + apr_pool_cleanup_register(pool, conn, cleanup_ssl, apr_pool_cleanup_null); + + conn->ssl = SSL_new(ssl_context); + if (conn->ssl == NULL) + return svn_error_create(SVN_ERR_RA_SVN_SSL_INIT, NULL, + "Could not create a SSL from the SSL context"); + + if (!BIO_new_bio_pair(&conn->internal_bio, 8192, &conn->network_bio, 8192)) + return svn_error_create(SVN_ERR_RA_SVN_SSL_INIT, NULL, + "Could not create a a new BIO pair"); + + SSL_set_bio(conn->ssl, conn->internal_bio, conn->internal_bio); + + return SVN_NO_ERROR; +} + +svn_error_t *svn_ra_svn_ssl_accept(svn_ra_svn_conn_t *conn, apr_pool_t *pool) +{ + SVN_ERR(do_ssl_operation(conn, SSL_accept, NULL, NULL, NULL, NULL)); + + /* Now SSL has been setup, and can be used. */ + conn->use_ssl = TRUE; + + return SVN_NO_ERROR; +} + +svn_error_t *svn_ra_svn_ssl_connect(svn_ra_svn_conn_t *conn, apr_pool_t *pool) +{ + SVN_ERR(do_ssl_operation(conn, SSL_connect, NULL, NULL, NULL, NULL)); + + /* Now SSL has been setup, and can be used. */ + conn->use_ssl = TRUE; + + return SVN_NO_ERROR; +} Index: subversion/libsvn_ra_svn/ra_svn.h =================================================================== --- subversion/libsvn_ra_svn/ra_svn.h (revision 11465) +++ subversion/libsvn_ra_svn/ra_svn.h (working copy) @@ -30,6 +30,9 @@ #include #include +#include +#include + /* Handler for blocked writes. */ typedef svn_error_t *(*ra_svn_block_handler_t)(svn_ra_svn_conn_t *conn, apr_pool_t *pool, @@ -54,6 +57,10 @@ void *block_baton; apr_hash_t *capabilities; apr_pool_t *pool; + svn_boolean_t use_ssl; /* Set to true after a SSL handshake is done */ + SSL *ssl; /* The SSL to use for this connection. */ + BIO *internal_bio; /* The Subversion/SSL side of a BIO pair. */ + BIO *network_bio; /* The network side of a BIO pair. */ }; /* Set a callback for blocked writes on conn. This handler may Index: subversion/svnserve/main.c =================================================================== --- subversion/svnserve/main.c (revision 11465) +++ subversion/svnserve/main.c (working copy) @@ -42,6 +42,8 @@ #include "server.h" +#include + /* The strategy for handling incoming connections. Some of these may be unavailable due to platform limitations. */ enum connection_handling_mode { @@ -95,6 +97,8 @@ #define SVNSERVE_OPT_FOREGROUND 258 #define SVNSERVE_OPT_TUNNEL_USER 259 #define SVNSERVE_OPT_VERSION 260 +#define SVNSERVE_OPT_CERT_FILE 261 +#define SVNSERVE_OPT_KEY_FILE 262 static const apr_getopt_option_t svnserve__options[] = { @@ -118,6 +122,10 @@ {"threads", 'T', 0, "use threads instead of fork"}, #endif {"listen-once", 'X', 0, "listen once (useful for debugging)"}, + {"cert-file", SVNSERVE_OPT_CERT_FILE, 1, + "public certificate for SSL"}, + {"key-file", SVNSERVE_OPT_KEY_FILE, 1, + "private key to certificate for SSL"}, {0, 0, 0, 0} }; @@ -215,7 +223,83 @@ return svn_ver_check_list (&my_version, checklist); } +/* Helper method for more verbose SSL errors. */ +static const char *ssl_last_error(apr_pool_t *pool) +{ + unsigned long ssl_error = ERR_get_error(); + char *buffer; + + if (ssl_error == 0) + return strerror(errno); + buffer = apr_pcalloc(pool, 256); + ERR_error_string_n(ssl_error, buffer, 256); + return buffer; +} + +/* Initializes the SSL context to be used by the server. */ +static svn_boolean_t init_ssl_ctx(apr_pool_t *pool, + serve_params_t *params, + const char *ssl_cert_file, + const char *ssl_key_file) +{ + /* List of ciphers that we allow for SSL connections. */ + const char *cipher_list = "ALL:!LOW"; + + SSL_load_error_strings(); + SSL_library_init(); + + /* TODO : Seed the randum number generator (RNG) + * for those operating systems that does not have /dev/urandom. + */ + + params->ssl_ctx = SSL_CTX_new(SSLv23_server_method()); + if (params->ssl_ctx == NULL) + { + fprintf(stderr, "SSL_CTX_new() failed.\n"); + return FALSE; + } + + if (SSL_CTX_set_cipher_list(params->ssl_ctx, cipher_list) != 1) + { + fprintf(stderr, "Failed to set cipher list to %s\n", cipher_list); + return FALSE; + } + + if (SSL_CTX_use_certificate_chain_file(params->ssl_ctx, ssl_cert_file) != 1) + { + fprintf(stderr, "Failed to load certificate file %s : %s", + ssl_cert_file, ssl_last_error(pool)); + return FALSE; + } + + if (SSL_CTX_use_RSAPrivateKey_file(params->ssl_ctx, ssl_key_file, + SSL_FILETYPE_PEM) != 1) + { + fprintf(stderr, "Failed to load key file %s : %s", + ssl_key_file, ssl_last_error(pool)); + return FALSE; + } + + + if (!SSL_CTX_check_private_key(params->ssl_ctx)) + { + fprintf(stderr, "Failed to verify key file %s : %s", + ssl_key_file, ssl_last_error(pool)); + return FALSE; + } + + return TRUE; +} + +/* Frees the allocated SSL context. */ +static void destroy_ssl_ctx(serve_params_t *params) +{ + if (params->ssl_ctx != NULL) + SSL_CTX_free(params->ssl_ctx); +} + + int main(int argc, const char *const *argv) { enum run_mode run_mode = run_mode_none; @@ -242,7 +326,9 @@ enum connection_handling_mode handling_mode = CONNECTION_DEFAULT; apr_uint16_t port = SVN_RA_SVN_PORT; const char *host = NULL; - + const char *ssl_cert_file = NULL; /* Path to the server public certificate. */ + const char *ssl_key_file = NULL; /* Path to server private key. */ + /* Initialize the app. */ if (svn_cmdline_init("svn", stderr) != EXIT_SUCCESS) return EXIT_FAILURE; @@ -266,6 +352,8 @@ params.tunnel = FALSE; params.tunnel_user = NULL; params.read_only = FALSE; + params.ssl_layer = FALSE; + params.ssl_ctx = NULL; while (1) { status = apr_getopt_long(os, svnserve__options, &opt, &arg); @@ -336,11 +424,49 @@ case 'T': handling_mode = connection_mode_thread; break; + + case SVNSERVE_OPT_CERT_FILE: + SVN_INT_ERR(svn_utf_cstring_to_utf8(&ssl_cert_file, arg, pool)); + ssl_cert_file = svn_path_internal_style(ssl_cert_file, pool); + SVN_INT_ERR(svn_path_get_absolute(&ssl_cert_file, ssl_cert_file, pool)); + break; + + case SVNSERVE_OPT_KEY_FILE: + SVN_INT_ERR(svn_utf_cstring_to_utf8(&ssl_key_file, arg, pool)); + ssl_key_file = svn_path_internal_style(ssl_key_file, pool); + SVN_INT_ERR(svn_path_get_absolute(&ssl_key_file, ssl_key_file, pool)); + break; } } if (os->ind != argc) usage(argv[0]); + if (ssl_cert_file != NULL || ssl_key_file != NULL) + { + if (ssl_cert_file == NULL) + { + fprintf(stderr, "Certificate file not specified for the key.\n"); + usage(argv[0]); + } + if (ssl_key_file == NULL) + { + fprintf(stderr, "Key file not specified for the certificate.\n"); + usage(argv[0]); + } + if (run_mode != run_mode_listen_once && + run_mode != run_mode_daemon) + { + fprintf(stderr, "Only daemon and listen-once is supported for SSL.\n"); + usage(argv[0]); + } + + if (!init_ssl_ctx(pool, ¶ms, ssl_cert_file, ssl_key_file)) + { + exit(1); + } + params.ssl_layer = TRUE; + } + if (params.tunnel_user && run_mode != run_mode_tunnel) { fprintf(stderr, "Option --tunnel-user is only valid in tunnel mode.\n"); @@ -521,5 +647,9 @@ } } + if (params.ssl_layer) + destroy_ssl_ctx(¶ms); + return 0; } + Index: subversion/svnserve/serve.c =================================================================== --- subversion/svnserve/serve.c (revision 11465) +++ subversion/svnserve/serve.c (working copy) @@ -59,6 +59,7 @@ const char *tunnel_user; /* Allow EXTERNAL to authenticate as this */ svn_boolean_t read_only; /* Disallow write access (global flag) */ int protocol_version; + SSL_CTX *ssl_ctx; /* SSL context to use for SSL socket layer */ } server_baton_t; typedef struct { @@ -1316,14 +1317,28 @@ b.user = NULL; b.cfg = NULL; /* Ugly; can drop when we remove v1 support. */ b.pwdb = NULL; /* Likewise */ + b.ssl_ctx = params->ssl_ctx; + err = SVN_NO_ERROR; + /* Send greeting. When we drop support for version 1, we can - * start sending an empty mechlist. */ - SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w(nn(!", "success", - (apr_uint64_t) 1, (apr_uint64_t) 2)); + * start sending an empty mechlist. + * Note : version 1 does not support SSL. */ + if (params->ssl_layer) + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w(nn(!", "success", + (apr_uint64_t) 2, (apr_uint64_t) 2)); + else + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w(nn(!", "success", + (apr_uint64_t) 1, (apr_uint64_t) 2)); + SVN_ERR(send_mechs(conn, pool, &b, READ_ACCESS)); - SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!)(w))", - SVN_RA_SVN_CAP_EDIT_PIPELINE)); + if (params->ssl_layer) + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!)(ww))", + SVN_RA_SVN_CAP_EDIT_PIPELINE, + SVN_RA_SVN_CAP_SSL)); + else + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!)(w))", + SVN_RA_SVN_CAP_EDIT_PIPELINE)); /* Read client response. Because the client response form changed * between version 1 and version 2, we have to do some of this by @@ -1342,6 +1357,9 @@ SVN_ERR(svn_ra_svn_parse_tuple(item->u.list, pool, "nw(?c)l", &ver, &mech, &mecharg, &caplist)); SVN_ERR(svn_ra_svn_set_capabilities(conn, caplist)); + if (svn_ra_svn_has_capability(conn, SVN_RA_SVN_CAP_SSL)) + return svn_error_create(SVN_ERR_RA_SVN_BAD_VERSION, NULL, + _("Client does not support SSL")); SVN_ERR(auth(conn, pool, mech, mecharg, &b, READ_ACCESS, &success)); if (!success) return svn_ra_svn_flush(conn, pool); @@ -1361,10 +1379,24 @@ else if (b.protocol_version == 2) { /* Version 2: client sends version, capability list, and client - * URL, and then we do an auth request. */ + * URL, and then we do an auth request after test for client + * SSL capability. */ SVN_ERR(svn_ra_svn_parse_tuple(item->u.list, pool, "nlc", &ver, &caplist, &client_url)); SVN_ERR(svn_ra_svn_set_capabilities(conn, caplist)); + + if (params->ssl_layer) + { + if (!svn_ra_svn_has_capability(conn, SVN_RA_SVN_CAP_SSL)) + return svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL, + _("Client must have SSL capability")); + + /* Flush write buffer before SSL handshake. */ + svn_ra_svn_flush(conn, pool); + + SVN_ERR(svn_ra_svn_ssl_init(conn, pool, b.ssl_ctx)); + SVN_ERR(svn_ra_svn_ssl_accept(conn, pool)); + } err = find_repos(client_url, params->root, &b, pool); if (!err) { Index: subversion/svnserve/server.h =================================================================== --- subversion/svnserve/server.h (revision 11465) +++ subversion/svnserve/server.h (working copy) @@ -23,6 +23,8 @@ #include +#include + #ifdef __cplusplus extern "C" { #endif /* __cplusplus */ @@ -45,6 +47,12 @@ /* True if the deprecated read-only flag was specified on the command-line, which forces all connections to be read-only. */ svn_boolean_t read_only; + + /* True if the connection is over SSL. */ + svn_boolean_t ssl_layer; + + /* If ssl_layer is true, points to the created SSL context. */ + SSL_CTX *ssl_ctx; } serve_params_t; /* Serve the connection CONN according to the parameters PARAMS. */