Skip site navigation (1)Skip section navigation (2)
Date:      Tue, 22 Jan 2013 17:32:57 GMT
From:      Michael Gmelin <freebsd@grem.de>
To:        freebsd-gnats-submit@FreeBSD.org
Subject:   kern/175514: [patch] Support PKI in libfetch for HTTP over SSL (https)
Message-ID:  <201301221732.r0MHWvYQ097564@red.freebsd.org>
Resent-Message-ID: <201301221740.r0MHe1Ek013121@freefall.freebsd.org>

next in thread | raw e-mail | index | archive | help

>Number:         175514
>Category:       kern
>Synopsis:       [patch] Support PKI in libfetch for HTTP over SSL (https)
>Confidential:   no
>Severity:       non-critical
>Priority:       low
>Responsible:    freebsd-bugs
>State:          open
>Quarter:        
>Keywords:       
>Date-Required:
>Class:          change-request
>Submitter-Id:   current-users
>Arrival-Date:   Tue Jan 22 17:40:00 UTC 2013
>Closed-Date:
>Last-Modified:
>Originator:     Michael Gmelin
>Release:        FreeBSD 9.1-RELEASE
>Organization:
Grem Equity GmbH
>Environment:
FreeBSD bsd64 9.1-RELEASE FreeBSD 9.1-RELEASE #3 r244944: Wed Jan  2 02:10:20 UTC 2013     root@bsd64:/usr/obj/usr/src/sys/GENERIC  amd64
>Description:
The original motivation for this patch was that pkgng uses libfetch as its
transport mechanism, so I took a closer look into how https is currently
implemented in libfetch. As it turns out support is minimal and handles
nothing more than basic encryption. It does not verify the certificate
against any trust store, does not validate the CN/subjectAltName or support
client certificates. It also allows SSLv2 by default and leaks resources. In
addition it can segfault in verbose mode.

I know that there resentments to the PKI model due to its various
shortcomings, since any CA can be break the trust (as we've seen in recent
history, e.g. DigiNotar). There are also new promising concepts on the
horizon (e.g. DANE, RFC6698), I still think supporting the current PKI model
makes sense for the following reasons:

1. It matches the user's expectation:
   If I "fetch https://www.google.com" I expect it to complain if the
   certificate presented is self-signed by fraudster.com.

2. It is solution that's been deployed already (and almost all fetch like tools
   and browsers have it enabled by default). Better solutions are not in
   widespread use yet.

3. It allows running your private CA for local use, including client
   certificate authentication. In this case most of the problems of a Public
   Key Infrastructure are not relevant.

Once libfetch is patched, the user either has to install trusted
certificates (e.g. by installing security/ca_root_nss) or explicitly disable
the checks. For the sake of migrating existing setups, it might make sense
to add a configuration file (e.g. /etc/fetch.conf) that disables checks by
default, so people's existing setups won't break - having such a default
configuration for libfetch in general might make sense anyway (also for
PROXY settings). I'm open to discuss if this is considered necessary and how
it should be implemented - basically I think the feature itself should be
enabled by default, otherwise it's of limited use.

That said, what this patch does is:
- Disable SSLv2 by default. Can be enabled by setting SSL_ALLOW_SSL2.
- Enable peer certificate verification (can be disabled by setting
  SSL_NO_VERIFY_PEER, CA trust store location can be specified by setting
  SSL_CA_CERTFILE and SSL_CA_CERTPATH and default to /etc/ssl/cert.pem)
- Enable hostname verification (CN, subjectAltName, DNS, wildcards, IPv4
  IPv6), based on RFC2818, RFC2459, RFC3280 and especially RFC6125. Can be
  disabled by setting SSL_NO_VERIFY_HOSTNAME
- Allow the use of certificate revocation list (can be configured by setting
  SSL_CRL_FILE)
- Allow the use of client certificate based authentication (by setting
  SSL_CLIENT_CERT_FILE and optionally SSL_CLIENT_KEY_FILE)
- Allow disabling of SSL methods by setting SSL_NO_SSL3 and SSL_NO_TLS1
- Fix a resource leak on multiple invocations of fetch_ssl
- Fix a segmentation fault when accessing a non-working host in verbose mode
- Document all these features in detail in fetch(3)
- Add new environment variables to the list in fetch(1) that refers to
  fetch(3)

The patch does not attempt to fix the thread-unsafe use of OpenSSL in libfetch
(ultimately the man page states that it is not fully reentrant). If there is
any interest I can fix this, but I would need some mentoring if there are
scenarios where the multithreading code shouldn't be run or not etc and if
it makes sense at all.

I tried following style(9) as closely as possible, but also respected the
slightly different style of libfetch. If there is something I overlooked
helpful criticism is always welcome (also if there are any other changes
that should be done to the code).

I tested the patch on FreeBSD 9.0 and 9.1 (amd64) using system gcc and clang.
Besides the patch to fetch.1, which barfs on 9.0 due to a glitch in the
manpage on 9.1, it should apply cleanly on both versions of the OS.

>How-To-Repeat:
fetch https://173.194.44.49
(which just works, even though it shouldn't since that IP is not
specified in google's certificate)

>Fix:
Apply patches supplied.

cd /usr/src
patch < /path/to/libfetch.patch.txt


Patch attached with submission follows:

Index: lib/libfetch/fetch.3
===================================================================
--- lib/libfetch/fetch.3	(revision 245791)
+++ lib/libfetch/fetch.3	(working copy)
@@ -1,5 +1,6 @@
 .\"-
 .\" Copyright (c) 1998-2011 Dag-Erling Smørgrav
+.\" Copyright (c) 2013 Michael Gmelin <freebsd@grem.de>
 .\" All rights reserved.
 .\"
 .\" Redistribution and use in source and binary forms, with or without
@@ -392,6 +393,60 @@
 library,
 .Fn fetchPutHTTP
 is currently unimplemented.
+.Sh HTTPS SCHEME
+Based on HTTP SCHEME. By default the peer is verified
+using the CA bundle located in
+.Pa /etc/ssl/cert.pem .
+The file may contain multiple CA certificates. A common source of a
+current CA bundle is
+.Pa \%security/ca_root_nss .
+.Pp
+The CA bundle used for peer verification can be changed by setting the
+environment variables
+.Ev SSL_CA_CERT_FILE
+to point to a concatenated
+bundle of trusted certificates and
+.Ev SSL_CA_CERT_PATH
+to point to a
+directory containing hashes of trusted CAs (see
+.Xr verify 1 ) .
+.Pp
+A certificate revocation list (CRL) can be used by setting the
+environment variable
+.Ev SSL_CRL_FILE
+(see
+.Xr crl 1 ) .
+.Pp
+Peer verification can be disabled by setting the environment variable
+.Ev SSL_NO_VERIFY_PEER .
+Note that this also disables CRL checking.
+.Pp
+By default the service identity is verified according to the rules
+detailed in RFC6125 (also known as hostname verification). This
+feature can be disabled by setting the environment variable
+.Ev SSL_NO_VERIFY_HOSTNAME .
+.Pp
+Client certificate based authentication is supported. The environment
+variable
+.Ev SSL_CLIENT_CERT_FILE
+should be set to point to a file
+containing key and client certificate to be used in PEM format. In
+case the key is stored in a separate file, the environment variable
+.Ev SSL_CLIENT_KEY_FILE
+can be set to point to the key in PEM format. In
+case the key uses a password, the user will be prompted on standard input (see
+.Xr PEM 3 ) .
+.Pp
+By default
+.Nm libfetch
+allows SSLv3 and TLSv1 when negotiating the connecting with the remote
+peer. You can change this behavior by setting the environment variable
+.Ev SSL_ALLOW_SSL2
+to allow SSLv2 (not recommended) and
+.Ev SSL_NO_SSL3
+or
+.Ev SSL_NO_TLS1
+to disable the respective methods.
 .Sh AUTHENTICATION
 Apart from setting the appropriate environment variables and
 specifying the user name and password in the URL or the
@@ -579,6 +634,31 @@
 Same as
 .Ev NO_PROXY ,
 for compatibility.
+.It Ev SSL_ALLOW_SSL2
+Allow SSL version 2 when negotiating the connection (not recommended).
+.It Ev SSL_CA_CERT_FILE
+CA certificate bundle containing trusted CA certificates. Default
+value:
+.Pa /etc/ssl/cert.pem .
+.It Ev SSL_CA_CERT_PATH
+Path containing trusted CA hashes.
+.It Ev SSL_CLIENT_CERT_FILE
+PEM encoded client certificate/key which will be used in
+client certificate authentication.
+.It Ev SSL_CLIENT_KEY_FILE
+PEM encoded client key in case key and client certificate
+are stored separately.
+.It Ev SSL_CRL_FILE
+File containing certificate revocation list.
+.It Ev SSL_NO_SSL3
+Don't allow SSL version 3 when negotiating the connection.
+.It Ev SSL_NO_TLS1
+Don't allow TLV version 1 when negotiating the connection.
+.It Ev SSL_NO_VERIFY_HOSTNAME
+If set, do not verify that the hostname matches the subject of the
+certificate presented by the server.
+.It Ev SSL_NO_VERIFY_PEER
+If set, do not verify the peer certificate against trusted CAs.
 .El
 .Sh EXAMPLES
 To access a proxy server on
@@ -610,6 +690,19 @@
 .Bd -literal -offset indent
 NO_PROXY=localhost,127.0.0.1
 .Ed
+.Pp
+Access HTTPS website without any certificate verification whatsoever:
+.Bd -literal -offset indent
+SSL_NO_VERIFY_PEER=1
+SSL_NO_VERIFY_HOSTNAME=1
+.Ed
+.Pp
+Access HTTPS website using client certificate based authentication
+and a private CA:
+.Bd -literal -offset indent
+SSL_CLIENT_CERT_FILE=/path/to/client.pem
+SSL_CA_CERT_FILE=/path/to/myca.pem
+.Ed
 .Sh SEE ALSO
 .Xr fetch 1 ,
 .Xr ftpio 3 ,
@@ -678,7 +771,8 @@
 .An Hajimu Umemoto Aq ume@FreeBSD.org ,
 .An Henry Whincup Aq henry@techiebod.com ,
 .An Jukka A. Ukkonen Aq jau@iki.fi ,
-.An Jean-Fran\(,cois Dockes Aq jf@dockes.org
+.An Jean-Fran\(,cois Dockes Aq jf@dockes.org ,
+.An Michael Gmelin Aq freebsd@grem.de
 and others.
 It replaces the older
 .Nm ftpio
@@ -688,7 +782,9 @@
 .An Jordan K. Hubbard Aq jkh@FreeBSD.org .
 .Pp
 This manual page was written by
-.An Dag-Erling Sm\(/orgrav Aq des@FreeBSD.org .
+.An Dag-Erling Sm\(/orgrav Aq des@FreeBSD.org
+and
+.An Michael Gmelin Aq freebsd@grem.de .
 .Sh BUGS
 Some parts of the library are not yet implemented.
 The most notable
@@ -717,6 +813,10 @@
 .Fn fetchStatFTP
 does not check that the result of an MDTM command is a valid date.
 .Pp
+In case password protected keys are used for client certificate based
+authentication the user is prompted for the password on each and every
+fetch operation.
+.Pp
 The man page is incomplete, poorly written and produces badly
 formatted text.
 .Pp
Index: lib/libfetch/http.c
===================================================================
--- lib/libfetch/http.c	(revision 245791)
+++ lib/libfetch/http.c	(working copy)
@@ -1392,7 +1392,7 @@
 		/* fetch_connect() has already set an error code */
 		return (NULL);
 	if (strcasecmp(URL->scheme, SCHEME_HTTPS) == 0 &&
-	    fetch_ssl(conn, verbose) == -1) {
+			fetch_ssl(conn, URL, verbose) == -1) {
 		fetch_close(conn);
 		/* grrr */
 		errno = EAUTH;
Index: lib/libfetch/common.c
===================================================================
--- lib/libfetch/common.c	(revision 245791)
+++ lib/libfetch/common.c	(working copy)
@@ -1,5 +1,6 @@
 /*-
  * Copyright (c) 1998-2011 Dag-Erling Smørgrav
+ * Copyright (c) 2013 Michael Gmelin <freebsd@grem.de>
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -47,6 +48,10 @@
 #include <string.h>
 #include <unistd.h>
 
+#ifdef WITH_SSL
+#include <openssl/x509v3.h>
+#endif
+
 #include "fetch.h"
 #include "common.h"
 
@@ -317,12 +322,470 @@
 	return (conn);
 }
 
+#ifdef WITH_SSL
+/*
+ * Callback for SSL certificate verification, this is called on server cert
+ * verification. It takes no decision, but informs the user in case
+ * verification failed.
+ */
+int
+fetch_ssl_cb_verify_crt(int verified, X509_STORE_CTX *ctx)
+{
+	char *str;
 
+	if (!verified)
+	{
+		str = X509_NAME_oneline(
+		    X509_get_subject_name(
+			X509_STORE_CTX_get_current_cert(ctx)),
+			0, 0);
+		fprintf(stderr, "Certificate verification failed %s\n",
+		    str);
+		OPENSSL_free(str);
+	}
+	return (verified);
+}
+
 /*
+ * Convert characters A-Z to lowercase (intentionally avoid any
+ * locale specific conversions)
+ */
+static char
+fetch_ssl_tolower(char in)
+{
+	if (in >= 'A' && in <= 'Z')
+		return (in + 32);
+	else
+		return (in);
+}
+
+/*
+ * Check if passed hostnames a and b are equal
+ */
+static int
+fetch_ssl_hostname_equal(const char *a, size_t alen, const char *b,
+    size_t blen)
+{
+	size_t i;
+
+	if (alen != blen)
+		return (0);
+	for (i=0; i<alen; ++i) {
+		if (fetch_ssl_tolower(a[i]) != fetch_ssl_tolower(b[i]))
+			return (0);
+	}
+	return (1);
+}
+
+/*
+ * Check if domain label is traditional, meaning that only A-Z, a-z, 0-9 and
+ * '-' (hyphen) are allowed. Hyphens have to be surrounded by alphanumeric
+ * characters. Double hyphens (like they're found in IDN a-labels 'xn--') are
+ * not allowed.
+ */
+static int
+fetch_ssl_is_traditional_domain_label(const char *label, size_t len,
+    int allow_wildcard)
+{
+	size_t i;
+
+	for (i=0; i<len; ++i) {
+		if (!((label[i] >= '0' && label[i] <= '9') ||
+			(label[i] >= 'A' && label[i] <= 'Z') ||
+			(label[i] >= 'a' && label[i] <= 'z') ||
+			(label[i] == '-' && i != 0 && i != len - 1
+			    && label[i - 1] != '-') ||
+			(label[i] == '*' && allow_wildcard)))
+			return (0);
+	}
+	return (1);
+}
+
+/*
+ * Check if host name consists only of numbers. This might indicate
+ * an IP address, which is not a good idea for CN wildcard comparison.
+ */
+static int
+fetch_ssl_hostname_is_only_numbers(const char *hostname, size_t len)
+{
+	size_t i;
+
+	for (i=0; i<len; ++i) {
+		if (!((hostname[i] >= '0' && hostname[i] <= '9') ||
+			hostname[i] == '.'))
+			return (0);
+	}
+	return (1);
+}
+
+/*
+ * Check if the host name passed matches the pattern passed in matches which
+ * is usually part of subjectAltName or CN of a certificate presented to the
+ * client. This includes wildcard matching. The algorithm is based on RFC6125,
+ * sections 6.4.3 and 7.2, which clarifies RFC2818 and RFC3280.
+ */
+static int
+fetch_ssl_hostname_match(const char *hostname, size_t hostnamelen,
+    const char *matches, size_t matcheslen)
+{
+	const char *wildcard;
+	const char *firstmatchdot;
+	const char *secondmatchdot;
+	const char *firsthostdot;
+
+	if (hostname && *hostname && matches && *matches)
+	{
+		if (!(wildcard = strnstr(matches, "*", matcheslen)))
+			return (fetch_ssl_hostname_equal(hostname,
+			    hostnamelen, matches, matcheslen));
+
+		/*
+		 * make sure hostname is not just dots and numbers
+		 */
+		if (fetch_ssl_hostname_is_only_numbers(hostname, hostnamelen))
+			return (0);
+
+		/* only one wildcard allowed in pattern */
+		if (strnstr(wildcard + 1, "*",
+			matcheslen - (wildcard - matches) - 1) != NULL)
+			return (0);
+
+		/*
+		 * make sure there are at least two more domain labels
+		 * and wildcard is in the leftmost label
+		 */
+		if ((firstmatchdot =
+			strnstr(matches, ".", matcheslen)) == NULL ||
+		    (matcheslen - (firstmatchdot - matches)) < 4 ||
+		    firstmatchdot < wildcard) /* important! */
+			return (0);
+
+		if ((secondmatchdot = strnstr(firstmatchdot + 1, ".",
+			    matcheslen - (firstmatchdot - matches) - 1)) == NULL ||
+		    (matcheslen - (secondmatchdot - matches)) < 2)
+			return (0);
+
+		/*
+		 * make sure hostname contains a dot and it's not the first
+		 * character
+		 */
+		if ((firsthostdot =
+			strnstr(hostname, ".", hostnamelen)) == NULL ||
+		    firsthostdot == hostname)
+			return (0);
+
+		/*
+		 * make sure host part of hostname is at least as long as
+		 * pattern it's supposed to match
+		 */
+		if ((firsthostdot - hostname) < (firstmatchdot - matches))
+			return (0);
+
+		/*
+		 * don't allow wildcards in non-traditional domain names
+		 * (IDN, A-label, U-label...)
+		 */
+		if (!fetch_ssl_is_traditional_domain_label(hostname,
+			firsthostdot - hostname - 1, 0) ||
+		    !fetch_ssl_is_traditional_domain_label(matches,
+			firstmatchdot - matches - 1, 1))
+			return (0);
+
+		/* match domain part (part after first dot) */
+		if (!fetch_ssl_hostname_equal(firsthostdot,
+			hostnamelen - (firsthostdot - hostname),
+			firstmatchdot, matcheslen - (firstmatchdot - matches)))
+			return (0);
+
+		/* match part left of wildcard */
+		if (!fetch_ssl_hostname_equal(hostname, wildcard - matches,
+			matches, wildcard - matches))
+			return (0);
+
+		/* match part right of wildcard */
+		if (!fetch_ssl_hostname_equal(
+			firsthostdot - (firstmatchdot - wildcard - 1),
+			    (firstmatchdot - wildcard - 1),
+			    firstmatchdot - (firstmatchdot - wildcard - 1),
+			    (firstmatchdot - wildcard - 1)))
+			return (0);
+
+		/* all tests succeded, it's a match */
+		return (1);
+	}
+	return (0);
+}
+
+/*
+ * Get numeric host address info - returns NULL if host was not an IP address.
+ * The caller is responsible for deallocation using freeaddrinfo(3)
+ */
+static struct addrinfo *
+fetch_ssl_get_numeric_addrinfo(const char *hostname, size_t len)
+{
+	struct addrinfo hints;
+	struct addrinfo *res;
+	char *host;
+
+	host = (char*) malloc(len + 1);
+	memcpy(host, hostname, len);
+	host[len] = '\0';
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_family = PF_UNSPEC;
+	hints.ai_socktype = SOCK_STREAM;
+	hints.ai_protocol = 0;
+	hints.ai_flags = AI_NUMERICHOST;
+	/* port is not relevant for this purpose */
+	getaddrinfo(host, "443", &hints, &res);
+	free(host);
+	return res;
+}
+
+/*
+ * Compare ip address in addrinfo with address passes
+ */
+static int
+fetch_ssl_ipaddress_match_binary(const struct addrinfo *hostaaddr,
+    const char *hostbaddr, size_t hostbaddrlen)
+{
+	if (hostaaddr->ai_family == AF_INET && hostbaddrlen == 4 &&
+	    !memcmp((void*)&((struct sockaddr_in*)(void*)(hostaaddr->ai_addr))->
+		sin_addr.s_addr, (const void*)hostbaddr, hostbaddrlen))
+		return (1);
+#ifdef INET6
+	else if (hostaaddr->ai_family == AF_INET6 && hostbaddrlen == 16 &&
+	    !memcmp((void*)&((struct sockaddr_in6*)(void*)hostaaddr->ai_addr)->
+		sin6_addr, (const void*)hostbaddr, hostbaddrlen))
+		return (1);
+#endif
+	return (0);
+}
+
+/*
+ * Compare ip address in addrinfo with host passed. If host is not an
+ * IP address, comparison will fail
+ */
+static int
+fetch_ssl_ipaddress_match(const struct addrinfo *hostaaddr, const char *hostb,
+    size_t hostblen)
+{
+	struct addrinfo *hostbaddr;
+	int ret = 0;
+
+	if ((hostbaddr = fetch_ssl_get_numeric_addrinfo(hostb, hostblen)) != NULL) {
+		if (hostaaddr->ai_family == AF_INET)
+			ret = fetch_ssl_ipaddress_match_binary(hostaaddr,
+			    (char*)&((struct sockaddr_in*)(void*)hostbaddr->
+				ai_addr)->sin_addr.s_addr, 4);
+#ifdef INET6
+		else if (hostaaddr->ai_family == AF_INET6)
+			ret = fetch_ssl_ipaddress_match_binary(hostaaddr,
+			    (char*)&((struct sockaddr_in6*)(void*)hostbaddr->
+				ai_addr)->sin6_addr, 16);
+#endif
+		freeaddrinfo(hostbaddr);
+	}
+	return (ret);
+}
+
+/*
+ * Verify server certificate subjectAltName/CN matches hostname
+ */
+static int
+fetch_ssl_verify_hostname(conn_t *conn, const struct url *URL)
+{
+	int i;
+	STACK_OF(GENERAL_NAME) *subject_alt_names = NULL;
+	unsigned char *cn = NULL;
+	int ret = 0;
+	struct addrinfo *hostipaddr = NULL;
+
+	hostipaddr = fetch_ssl_get_numeric_addrinfo(URL->host, strlen(URL->host));
+	if ((subject_alt_names = X509_get_ext_d2i(conn->ssl_cert,
+		    NID_subject_alt_name, NULL, NULL))) {
+		for (i=0; i<sk_GENERAL_NAME_num(subject_alt_names); ++i)
+		{
+			const GENERAL_NAME *name;
+			const char *namestr;
+			size_t namestrlen;
+			    
+			/*
+			 * This is a workaround, since the following line causes
+			 * alignment issues on clang:
+			 * name = sk_GENERAL_NAME_value(subject_alt_names, i);
+			 * OpenSSL explicitly warns to use those macros
+			 * directly, but there isn't much choice (and there
+			 * shouldn't be any ill side effects)
+			 */
+			name = (GENERAL_NAME*)SKM_sk_value(void,
+			    subject_alt_names, i);
+			namestr = (const char *) ASN1_STRING_data(name->d.ia5);
+			namestrlen = (size_t) ASN1_STRING_length(name->d.ia5);
+
+			if ((name->type == GEN_DNS &&
+				!hostipaddr &&
+				fetch_ssl_hostname_match(URL->host,
+				    strlen(URL->host), namestr, namestrlen)) ||
+			    (name->type == GEN_IPADD &&
+				hostipaddr &&
+				fetch_ssl_ipaddress_match_binary(hostipaddr,
+				    namestr, namestrlen))) {
+				ret = 1;
+				goto ssl_verify_done;
+			}
+		}
+	}
+	else { /* get most specific CN (last entry in list) and compare */
+		X509_NAME *subject;
+		int lastpos;
+		int loc = -1;
+
+		subject = X509_get_subject_name(conn->ssl_cert);
+		lastpos = -1;
+		loc = -1;
+		while ((lastpos = X509_NAME_get_index_by_NID(subject,
+			    NID_commonName, lastpos)) != -1) {
+			loc = lastpos;
+		}
+		if (loc > -1) {
+			int cnlen;
+
+			cnlen = ASN1_STRING_to_UTF8(&cn,
+			    X509_NAME_ENTRY_get_data(
+				X509_NAME_get_entry(subject, loc)));
+			if ((!hostipaddr && fetch_ssl_hostname_match(
+				URL->host, strlen(URL->host), cn, cnlen)) ||
+			    (hostipaddr && fetch_ssl_ipaddress_match(
+				hostipaddr, cn, cnlen))) {
+				ret = 1;
+				goto ssl_verify_done;
+			}
+		}
+	}
+
+ssl_verify_done:
+	if (cn)
+		OPENSSL_free(cn);
+	if (hostipaddr)
+		freeaddrinfo(hostipaddr);
+	if (subject_alt_names)
+		GENERAL_NAMES_free(subject_alt_names);
+	return (ret);
+}
+
+/*
+ * Configure transport security layer based on environment
+ */
+static void
+fetch_ssl_setup_transport_layer(SSL_CTX *ctx, int verbose)
+{
+	long ssl_ctx_options;
+	
+	ssl_ctx_options = SSL_OP_ALL | SSL_OP_NO_TICKET;
+	if (!getenv("SSL_ALLOW_SSL2"))
+		ssl_ctx_options |= SSL_OP_NO_SSLv2;
+	if (getenv("SSL_NO_SSL3"))
+		ssl_ctx_options |= SSL_OP_NO_SSLv3;
+	if (getenv("SSL_NO_TLS1"))
+		ssl_ctx_options |= SSL_OP_NO_TLSv1;
+	if (verbose)
+		fetch_info("SSL options: %x", ssl_ctx_options);
+	SSL_CTX_set_options(ctx, ssl_ctx_options);
+}
+
+
+/*
+ * Configure peer verification based on environment
+ */
+static int
+fetch_ssl_setup_peer_verification(SSL_CTX *ctx, int verbose)
+{
+	X509_LOOKUP *crl_lookup;
+	const char *ca_cert_file;
+	const char *ca_cert_path;
+	const char *crl_file;
+
+	if (!getenv("SSL_NO_VERIFY_PEER")) {
+		ca_cert_file = getenv("SSL_CA_CERT_FILE")?
+		    getenv("SSL_CA_CERT_FILE"):"/etc/ssl/cert.pem";
+		ca_cert_path = getenv("SSL_CA_CERT_PATH");
+		if (verbose) {
+			fetch_info("Peer verification enabled");
+			if (ca_cert_file)
+				fetch_info("Using CA cert file: %s",
+				    ca_cert_file);
+			if (ca_cert_path)
+				fetch_info("Using CA cert path: %s",
+				    ca_cert_path);
+		}
+		SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER,
+		    fetch_ssl_cb_verify_crt);
+		SSL_CTX_load_verify_locations(ctx, ca_cert_file, ca_cert_path);
+		if ((crl_file = getenv("SSL_CRL_FILE")) != NULL) {
+			if (verbose)
+				fetch_info("Using CRL file: %s",
+				    crl_file);
+			crl_lookup = X509_STORE_add_lookup(
+			    SSL_CTX_get_cert_store(ctx), X509_LOOKUP_file());
+			if (crl_lookup == NULL ||
+			    !X509_load_crl_file(crl_lookup, crl_file,
+				X509_FILETYPE_PEM)) {
+				fprintf(stderr,
+				    "Could not load CRL file %s\n",
+				    crl_file);
+				return (0);
+			}
+			X509_STORE_set_flags(
+			    SSL_CTX_get_cert_store(ctx), X509_V_FLAG_CRL_CHECK |
+				X509_V_FLAG_CRL_CHECK_ALL);
+		}
+	}
+	return (1);
+}
+
+/*
+ * Configure client certificate based on environment
+ */
+static int
+fetch_ssl_setup_client_certificate(SSL_CTX *ctx, int verbose)
+{
+	const char *client_cert_file;
+	const char *client_key_file;
+
+	if ((client_cert_file = getenv("SSL_CLIENT_CERT_FILE")) != NULL) {
+		client_key_file = getenv("SSL_CLIENT_KEY_FILE")?
+		    getenv("SSL_CLIENT_KEY_FILE"):client_cert_file;
+		if (verbose) {
+			fetch_info("Using client cert file: %s",
+			    client_cert_file);
+			fetch_info("Using client key file: %s",
+			    client_key_file);
+		}
+		if (SSL_CTX_use_certificate_chain_file(ctx,
+			client_cert_file) != 1) {
+			fprintf(stderr,
+			    "Could not load client certificate %s\n",
+			    client_cert_file);
+			return (0);
+		}
+		if (SSL_CTX_use_PrivateKey_file(ctx, client_key_file,
+			SSL_FILETYPE_PEM) != 1) {
+			fprintf(stderr,
+			    "Could not load client key %s\n",
+			    client_key_file);
+			return (0);
+		}
+	}
+	return (1);
+}
+
+#endif
+
+/*
  * Enable SSL on a connection.
  */
 int
-fetch_ssl(conn_t *conn, int verbose)
+fetch_ssl(conn_t *conn, const struct url *URL, int verbose)
 {
 #ifdef WITH_SSL
 	int ret, ssl_err;
@@ -339,8 +802,14 @@
 	conn->ssl_ctx = SSL_CTX_new(conn->ssl_meth);
 	SSL_CTX_set_mode(conn->ssl_ctx, SSL_MODE_AUTO_RETRY);
 
+	fetch_ssl_setup_transport_layer(conn->ssl_ctx, verbose);
+	if (!fetch_ssl_setup_peer_verification(conn->ssl_ctx, verbose))
+		return (-1);
+	if (!fetch_ssl_setup_client_certificate(conn->ssl_ctx, verbose))
+		return (-1);
+
 	conn->ssl = SSL_new(conn->ssl_ctx);
-	if (conn->ssl == NULL){
+	if (conn->ssl == NULL) {
 		fprintf(stderr, "SSL context creation failed\n");
 		return (-1);
 	}
@@ -353,22 +822,38 @@
 			return (-1);
 		}
 	}
+	conn->ssl_cert = SSL_get_peer_certificate(conn->ssl);
 
+	if (!conn->ssl_cert) {
+		fprintf(stderr, "No server SSL certificate\n");
+		return (-1);
+	}
+
+	if (!getenv("SSL_NO_VERIFY_HOSTNAME")) {
+		if (verbose)
+			fetch_info("Verify hostname");
+		if (!fetch_ssl_verify_hostname(conn, URL)) {
+			fprintf(stderr,
+			    "SSL certificate subject doesn't match hostname %s\n",
+			    URL->host);
+			return (-1);
+		}
+	}
+
 	if (verbose) {
 		X509_NAME *name;
 		char *str;
 
-		fprintf(stderr, "SSL connection established using %s\n",
+		fetch_info("SSL connection established using %s",
 		    SSL_get_cipher(conn->ssl));
-		conn->ssl_cert = SSL_get_peer_certificate(conn->ssl);
 		name = X509_get_subject_name(conn->ssl_cert);
-		str = X509_NAME_oneline(name, 0, 0);
-		printf("Certificate subject: %s\n", str);
-		free(str);
-		name = X509_get_issuer_name(conn->ssl_cert);
-		str = X509_NAME_oneline(name, 0, 0);
-		printf("Certificate issuer: %s\n", str);
-		free(str);
+        	str = X509_NAME_oneline(name, 0, 0);
+        	fetch_info("Certificate subject: %s", str);
+        	OPENSSL_free(str);
+        	name = X509_get_issuer_name(conn->ssl_cert);
+        	str = X509_NAME_oneline(name, 0, 0);
+        	fetch_info("Certificate issuer: %s", str);
+        	OPENSSL_free(str);
 	}
 
 	return (0);
@@ -726,6 +1211,22 @@
 
 	if (--conn->ref > 0)
 		return (0);
+#ifdef WITH_SSL
+	if (conn->ssl) {
+		SSL_shutdown(conn->ssl);
+		SSL_set_connect_state(conn->ssl);
+		SSL_free(conn->ssl);
+		conn->ssl = NULL;
+	}
+	if (conn->ssl_ctx) {
+		SSL_CTX_free(conn->ssl_ctx);
+		conn->ssl_ctx = NULL;
+	}
+	if (conn->ssl_cert) {
+		X509_free(conn->ssl_cert);
+		conn->ssl_cert = NULL;
+	}
+#endif
 	ret = close(conn->sd);
 	free(conn->cache.buf);
 	free(conn->buf);
Index: lib/libfetch/common.h
===================================================================
--- lib/libfetch/common.h	(revision 245791)
+++ lib/libfetch/common.h	(working copy)
@@ -87,7 +87,10 @@
 conn_t		*fetch_connect(const char *, int, int, int);
 conn_t		*fetch_reopen(int);
 conn_t		*fetch_ref(conn_t *);
-int		 fetch_ssl(conn_t *, int);
+#ifdef WITH_SSL
+int		 fetch_ssl_cb_verify_crt(int, X509_STORE_CTX*);
+#endif
+int		 fetch_ssl(conn_t *, const struct url *, int);
 ssize_t		 fetch_read(conn_t *, char *, size_t);
 int		 fetch_getln(conn_t *);
 ssize_t		 fetch_write(conn_t *, const char *, size_t);
Index: usr.bin/fetch/fetch.1
===================================================================
--- usr.bin/fetch/fetch.1	(revision 245791)
+++ usr.bin/fetch/fetch.1	(working copy)
@@ -254,8 +254,18 @@
 .Ev HTTP_REFERER ,
 .Ev HTTP_USER_AGENT ,
 .Ev NETRC ,
-.Ev NO_PROXY No and
-.Ev no_proxy .
+.Ev NO_PROXY ,
+.Ev no_proxy ,
+.Ev SSL_ALLOW_SSL2 ,
+.Ev SSL_CA_CERT_FILE ,
+.Ev SSL_CA_CERT_PATH ,
+.Ev SSL_CLIENT_CERT_FILE ,
+.Ev SSL_CLIENT_KEY_FILE ,
+.Ev SSL_CRL_FILE ,
+.Ev SSL_NO_SSL3 ,
+.Ev SSL_NO_TLS1 ,
+.Ev SSL_NO_VERIFY_HOSTNAME and
+.Ev SSL_NO_VERIFY_PEER .
 .Sh EXIT STATUS
 The
 .Nm


>Release-Note:
>Audit-Trail:
>Unformatted:



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?201301221732.r0MHWvYQ097564>