From Mon Sep 17 00:00:00 2001
From: Matt McCutchen <matt@mattmccutchen.net>
Date: Tue, 15 Feb 2011 23:38:48 -0500

DANE prototype implementation.

See documentation in security/nss/README.dane .
---
 security/nss/README.dane           |   78 +++++++++++++
 security/nss/cmd/tstclnt/tstclnt.c |    7 +-
 security/nss/lib/ssl/config.mk     |    3 +
 security/nss/lib/ssl/manifest.mn   |    1 +
 security/nss/lib/ssl/ssl.def       |    6 +
 security/nss/lib/ssl/ssl.h         |    3 +
 security/nss/lib/ssl/sslauth.c     |    9 ++
 security/nss/lib/ssl/ssldane.c     |  218 ++++++++++++++++++++++++++++++++++++
 security/nss/lib/ssl/sslerr.h      |    7 +
 security/nss/lib/ssl/sslimpl.h     |    4 +
 security/nss/lib/ssl/sslsecur.c    |   26 +++++
 security/nss/lib/ssl/sslsock.c     |    1 +
 12 files changed, 362 insertions(+), 1 deletions(-)

diff --git a/security/nss/README.dane b/security/nss/README.dane
new file mode 100644
index 0000000..ecbf442
--- /dev/null
+++ b/security/nss/README.dane
@@ -0,0 +1,78 @@
+This modified version of NSS contains a proof-of-concept implementation of the
+draft DANE protocol (see https://tools.ietf.org/wg/dane/) for DNSSEC-based
+authentication of TLS services.  The implementation uses the libunbound DNSSEC
+stub resolver (http://unbound.net/).  It is far from ready for production or
+upstream consideration, but it should be fun for experimentation.
+
+When the environment variable NSS_SSL_DANE_ENABLE is set to 1, the behavior of
+SSL_AuthCertificate changes to query for a TLSA RRset for the requested service.
+If NSS gets a "secure" response that the RRset exists, the RRset is used
+(exclusively).  If NSS gets a "secure" response that the RRset does not exist,
+or an "insecure" response, it falls back to the traditional CERT_VerifyCertNow +
+CERT_VerifyHostName.  If the response is "bogus", server authentication fails in
+order to prevent downgrade attacks.
+
+To use:
+
+- To build the modified NSS, you need libunbound.so and unbound.h installed.
+  If they are not in the standard locations, I believe you can set OS_LIBS and
+  INCLUDES in security/nss/lib/ssl/config.mk .
+
+- Pass the environment variable NSS_SSL_DANE_ENABLE=1 to enable DANE.
+
+- Prepare an Unbound configuration file (see the unbound.conf(5) man page)
+  containing the DNSSEC trust anchors and any other settings you wish to use,
+  and pass its path in UNBOUND_CONFIG.  You can get the current DS record for
+  the IANA root zone from https://data.iana.org/root-anchors/root-anchors.xml .
+
+- If the DNS servers in your /etc/resolv.conf support DNSSEC, you can pass
+  UNBOUND_RESOLVCONF=/etc/resolv.conf .  If you have access to another recursive
+  DNSSEC server, you can specify it as a forward-zone of "." in the Unbound
+  configuration file.  Otherwise, your client will load the root servers.
+
+- The TLSA RRtype has not been assigned an official number.  Set TLSA_RRTYPE to
+  the number you wish to use (no TYPE prefix).
+
+- In DANE, services are identified by host name, transport protocol, and port
+  number.  The host name is specified via SSL_SetURL, as always.  The transport
+  protocol is fixed to "tcp" for now, though one might conceivably want to layer
+  NSS TLS on a custom socket that implements some other transport protocol.
+  The port number must be specified via the new API function SSL_SetServicePort,
+  or server authentication will fail.  tstclnt has been modified to call
+  SSL_SetServicePort.  To support existing applications that do not call
+  SSL_SetServicePort, you can set NSS_SSL_DANE_PORT to:
+
+  - A port number to always assume that port
+  - "socket" to assume the remote port number of the lower layer socket (correct
+    for a direct socket connection, wrong when using a proxy)
+
+Limitations / TODO:
+
+- The only supported certificate and hash types are the SHA-1 of an end-entity
+  certificate (0101) and a dummy record to assert the nonexistence of a TLS
+  service (0000).  Implement more hash types (will just need to cache the hashes
+  rather than precompute them all) and the certificate types that involve PKIX
+  validation.
+
+- Implement following of CNAMEs at the host name
+  (http://www.ietf.org/mail-archive/web/keyassure/current/msg01630.html).
+
+- The transport protocol name is fixed to "tcp".  Allow the application to
+  change it.
+
+- NSS will call the bad cert hook after a server authentication failure, as
+  always.  Consider making the failure harder for services that opt in to DANE.
+
+- Do the DNS queries through NSPR, and make it possible to either use the AD bit
+  of an external validator or hand the queries to libunbound in-process.
+
+- Modify PSM to call SSL_SetServicePort (should be easy, the data is right
+  there).
+
+- Modify PSM to not show unvalidated certificate details, and make sure EV works
+  properly.
+
+I grant unlimited copyright permission to copy, modify, and distribute this
+software, and I make absolutely no warranty.
+
+~ Matt McCutchen <matt@mattmccutchen.net>, 2011-02-15
diff --git a/security/nss/cmd/tstclnt/tstclnt.c b/security/nss/cmd/tstclnt/tstclnt.c
index 55684e6..620b429 100644
--- a/security/nss/cmd/tstclnt/tstclnt.c
+++ b/security/nss/cmd/tstclnt/tstclnt.c
@@ -205,6 +205,7 @@ static void Usage(const char *progName)
                     "", "");
     fprintf(stderr, "%-20s Hostname to connect with\n", "-h host");
     fprintf(stderr, "%-20s Port number for SSL server\n", "-p port");
+    fprintf(stderr, "%-20s Different service port number for DANE\n", "-P port");
     fprintf(stderr, 
             "%-20s Directory with cert database (default is ~/.netscape)\n",
 	    "-d certdir");
@@ -533,6 +534,7 @@ int main(int argc, char **argv)
     int                headerSeparatorPtrnId = 0;
     int                error = 0;
     PRUint16           portno = 443;
+    PRUint16           servicePortno = 0;
     char *             hs1SniHostName = NULL;
     char *             hs2SniHostName = NULL;
     PLOptState *optstate;
@@ -553,7 +555,7 @@ int main(int argc, char **argv)
     }
 
     optstate = PL_CreateOptState(argc, argv,
-                                 "23BSTW:a:c:d:fgh:m:n:op:qr:suvw:xz");
+                                 "23BSTW:a:c:d:fgh:m:n:op:P:qr:suvw:xz");
     while ((optstatus = PL_GetNextOpt(optstate)) == PL_OPT_OK) {
 	switch (optstate->option) {
 	  case '?':
@@ -600,6 +602,8 @@ int main(int argc, char **argv)
 
 	  case 'p': portno = (PRUint16)atoi(optstate->value);	break;
 
+	  case 'P': servicePortno = (PRUint16)atoi(optstate->value);	break;
+
 	  case 'q': pingServerFirst = PR_TRUE;          break;
 
 	  case 's': disableLocking = 1;                 break;
@@ -888,6 +892,7 @@ int main(int argc, char **argv)
     } else {
         SSL_SetURL(s, host);
     }
+    SSL_SetServicePort(s, servicePortno ? servicePortno : portno);
 
     /* Try to connect to the server */
     status = PR_Connect(s, &addr, PR_INTERVAL_NO_TIMEOUT);
diff --git a/security/nss/lib/ssl/config.mk b/security/nss/lib/ssl/config.mk
index c06ba31..c8dc71b 100644
--- a/security/nss/lib/ssl/config.mk
+++ b/security/nss/lib/ssl/config.mk
@@ -118,3 +118,6 @@ EXTRA_LIBS += $(ZLIB_LIBS)
 endif
 
 endif
+
+# DANE
+OS_LIBS += -lunbound
diff --git a/security/nss/lib/ssl/manifest.mn b/security/nss/lib/ssl/manifest.mn
index 8451229..e38bd89 100644
--- a/security/nss/lib/ssl/manifest.mn
+++ b/security/nss/lib/ssl/manifest.mn
@@ -74,6 +74,7 @@ CSRCS = \
 	nsskea.c \
 	sslinfo.c \
 	ssl3ecc.c \
+	ssldane.c \
 	$(NULL)
 
 LIBRARY_NAME = ssl
diff --git a/security/nss/lib/ssl/ssl.def b/security/nss/lib/ssl/ssl.def
index d3f455c..e080130 100644
--- a/security/nss/lib/ssl/ssl.def
+++ b/security/nss/lib/ssl/ssl.def
@@ -152,3 +152,9 @@ SSL_SNISocketConfigHook;
 ;+    local:
 ;+*;
 ;+};
+;+NSS_DANE {        # DANE additions ~ Matt
+;+    global:
+SSL_SetServicePort;
+;+    local:
+;+*;
+;+};
diff --git a/security/nss/lib/ssl/ssl.h b/security/nss/lib/ssl/ssl.h
index e294a67..72f6563 100644
--- a/security/nss/lib/ssl/ssl.h
+++ b/security/nss/lib/ssl/ssl.h
@@ -475,6 +475,9 @@ SSL_IMPORT SECStatus SSL_RedoHandshake(PRFileDesc *fd);
  */
 SSL_IMPORT SECStatus SSL_SetURL(PRFileDesc *fd, const char *url);
 
+/* Likewise, set the service port for DANE. ~ Matt */
+SSL_IMPORT SECStatus SSL_SetServicePort(PRFileDesc *fd, PRUint16 servicePort);
+
 /*
  * Allow an application to define a set of trust anchors for peer
  * cert validation.
diff --git a/security/nss/lib/ssl/sslauth.c b/security/nss/lib/ssl/sslauth.c
index 8028694..86e8e61 100644
--- a/security/nss/lib/ssl/sslauth.c
+++ b/security/nss/lib/ssl/sslauth.c
@@ -258,6 +258,15 @@ SSL_AuthCertificate(void *arg, PRFileDesc *fd, PRBool checkSig, PRBool isServer)
     /* this may seem backwards, but isn't. */
     certUsage = isServer ? certUsageSSLClient : certUsageSSLServer;
 
+    if (!isServer) {
+	rv = ssl_DANECheck(ss);
+	if (rv == SECFailure && PORT_GetError() == SSL_ERROR_DANE_INAPPLICABLE)
+	    /* Fall through to traditional checking. */
+	    ;
+	else
+	    return rv;
+    }
+
     rv = CERT_VerifyCertNow(handle, ss->sec.peerCert, checkSig, certUsage,
 			    ss->pkcs11PinArg);
 
diff --git a/security/nss/lib/ssl/ssldane.c b/security/nss/lib/ssl/ssldane.c
new file mode 100644
index 0000000..79fc079
--- /dev/null
+++ b/security/nss/lib/ssl/ssldane.c
@@ -0,0 +1,218 @@
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+#include <arpa/inet.h> /* ntohs */
+
+#include "nspr.h"
+#include "cert.h"
+#include "ssl.h"
+#include "sslimpl.h"
+#include "pk11func.h" /* PK11_HashBuf */
+
+#include <unbound.h>
+
+#define RRCLASS_IN 1
+#define BUG_CONST_CAST(_x) ((char *) _x)
+
+static PRBool enabled = PR_FALSE;
+
+static int tlsa_rrtype;
+static struct ub_ctx *ctx; /* XXX Free this on NSS shutdown */
+static PRBool initSuccess = PR_FALSE;
+
+static PRBool globalPort_useSocket = PR_FALSE;
+static PRUint16 globalPort = 0;
+
+void ssl_DANEInit() {
+	int rc;
+
+	const char *enableStr = getenv("NSS_SSL_DANE_ENABLE");
+	enabled = (enableStr != NULL && atoi(enableStr) > 0);
+	if (!enabled) {
+		SSL_DBG(("%d: DANE is disabled", SSL_GETPID()));
+		return;
+	}
+
+	const char *tlsa_rrtype_str = getenv("TLSA_RRTYPE");
+	if (!tlsa_rrtype_str) {
+		SSL_DBG(("%d: DANE initialization failed: TLSA_RRTYPE environment variable not passed", SSL_GETPID()));
+		return;
+	}
+	char *end;
+	tlsa_rrtype = strtol(tlsa_rrtype_str, &end, 0);
+	if (*end != '\0') {
+		SSL_DBG(("%d: DANE initialization failed: TLSA_RRTYPE is not a number", SSL_GETPID()));
+		return;
+	}
+	if (tlsa_rrtype >= 1 && tlsa_rrtype <= 65535 &&
+		!(tlsa_rrtype >= 128 && tlsa_rrtype <= 255))
+		/* good */
+		;
+	else {
+		SSL_DBG(("%d: DANE initialization failed: TLSA_RRTYPE is not a valid data RRtype", SSL_GETPID()));
+		return;
+	}
+
+	const char *globalPortStr = getenv("NSS_SSL_DANE_PORT");
+	if (globalPortStr) {
+		/* TODO Support "socket:443,465" style that uses the socket port
+		 * only if it is in the whitelist. */
+		if (strcmp(globalPortStr, "socket") == 0)
+			globalPort_useSocket = PR_TRUE;
+		else {
+			int p = strtol(globalPortStr, &end, 0);
+			if (*end == '\0' && ((PRUint16) p) == p) {
+				globalPort = (PRUint16) p;
+			} else {
+				SSL_DBG(("%d: DANE initialization warning: NSS_SSL_DANE_PORT is not valid", SSL_GETPID()));
+			}
+		}
+	}
+
+	ctx = ub_ctx_create();
+	if (ctx == NULL) {
+		SSL_DBG(("%d: DANE initialization failed: ub_ctx_create failed", SSL_GETPID()));
+		return;
+	}
+
+	const char *resolvconf_fname = getenv("UNBOUND_RESOLVCONF");
+	if (resolvconf_fname != NULL) {
+		if ((rc = ub_ctx_resolvconf(ctx, BUG_CONST_CAST(resolvconf_fname))) < 0) {
+			SSL_DBG(("%d: DANE initialization failed: ub_ctx_resolvconf(%s) failed: %s", SSL_GETPID(), resolvconf_fname, ub_strerror(rc)));
+			return;
+		}
+	}
+
+	const char *config_fname = getenv("UNBOUND_CONFIG");
+	if (config_fname == NULL) {
+		SSL_DBG(("%d: DANE initialization failed: UNBOUND_CONFIG environment variable not passed", SSL_GETPID()));
+		return;
+	}
+	if ((rc = ub_ctx_config(ctx, BUG_CONST_CAST(config_fname))) < 0) {
+		SSL_DBG(("%d: DANE initialization failed: ub_ctx_config(%s) failed: %s", SSL_GETPID(), config_fname, ub_strerror(rc)));
+		return;
+	}
+
+	SSL_DBG(("%d: DANE initialized successfully", SSL_GETPID()));
+	initSuccess = PR_TRUE;
+}
+
+SECStatus ssl_DANECheck(sslSocket *ss) {
+	int rc;
+	SECStatus rv; /* don't ask */
+
+	if (!enabled) {
+		PORT_SetError(SSL_ERROR_DANE_INAPPLICABLE);
+		return SECFailure;
+	}
+
+	if (!initSuccess) {
+		PORT_SetError(SSL_ERROR_DANE_INIT_FAILED);
+		return SECFailure;
+	}
+
+	const char *hostname = ss->url;
+	if (!hostname && !hostname[0]) {
+		PORT_SetError(SSL_ERROR_BAD_CERT_DOMAIN);
+		return SECFailure;
+	}
+	PRUint16 servicePort = ss->servicePort;
+	if (!servicePort) {
+		if (globalPort_useSocket)
+			servicePort = ntohs(ss->sec.ci.port);
+		else if (globalPort)
+			servicePort = globalPort;
+		else {
+			SSL_DBG(("%d: DANE[%d]: service port for %s is unset", SSL_GETPID(), ss->fd, hostname));
+			PORT_SetError(SSL_ERROR_DANE_PORT_UNSET);
+			return SECFailure;
+		}
+	}
+
+	char *qname;
+	/* XXX Add a trailing dot? */
+	rc = asprintf(&qname, "_%d._tcp.%s", servicePort, hostname);
+	if (rc < 0) {
+		PORT_SetError(SEC_ERROR_NO_MEMORY);
+		return SECFailure;
+	}
+
+	struct ub_result *res;
+	if ((rc = ub_resolve(ctx, qname, tlsa_rrtype, RRCLASS_IN, &res)) < 0) {
+		SSL_DBG(("%d: DANE[%d]: ub_resolve(%s, TLSA) failed: %s", SSL_GETPID(), ss->fd, qname, ub_strerror(rc)));
+		PORT_SetError(SSL_ERROR_DANE_QUERY_FAILED);
+		goto out1;
+	}
+
+	if (res->bogus) {
+		SSL_DBG(("%d: DANE[%d]: %s result is bogus: %s", SSL_GETPID(), ss->fd, qname, res->why_bogus));
+		PORT_SetError(SSL_ERROR_DANE_QUERY_BOGUS);
+		goto out2;
+	}
+	if (!res->secure) {
+		SSL_DBG(("%d: DANE[%d]: %s result is insecure", SSL_GETPID(), ss->fd, qname));
+		PORT_SetError(SSL_ERROR_DANE_INAPPLICABLE);
+		goto out2;
+	}
+	if (!res->havedata) {
+		SSL_DBG(("%d: DANE[%d]: %s has no TLSA RRset", SSL_GETPID(), ss->fd, qname));
+		PORT_SetError(SSL_ERROR_DANE_INAPPLICABLE);
+		goto out2;
+	}
+
+	CERTCertificate *cert = ss->sec.peerCert;
+	unsigned char cert_sha1[SHA1_LENGTH];
+	if ((rv = PK11_HashBuf(SEC_OID_SHA1, cert_sha1, cert->derCert.data, cert->derCert.len)) != SECSuccess) {
+		/* XXX Does this clobber PORT_GetError()?  If not, enable it. */
+		/*SSL_DBG(("%d: DANE[%d]: failed to hash certificate", SSL_GETPID(), ss->fd));*/
+		goto out2;
+	}
+
+	int i;
+	for (i = 0; res->data[i] != NULL; i++) {
+		if (res->len[i] < 2) {
+			SSL_DBG(("%d: DANE[%d]: %s: invalid TLSA RR (short)", SSL_GETPID(), ss->fd, qname));
+			continue;
+		}
+		int ctype = res->data[i][0], htype = res->data[i][1];
+		switch (ctype) {
+		case 0:
+			/* dummy used to assert service nonexistence */
+			break;
+		case 1:
+			switch (htype) {
+			case 1: /* SHA-1 */
+				if (res->len[i] == 2 + SHA1_LENGTH) {
+					if (PORT_Memcmp(cert_sha1, res->data[i] + 2, SHA1_LENGTH) == 0) {
+						SSL_DBG(("%d: DANE[%d]: %s: cert matched TLSA SHA-1 RR", SSL_GETPID(), ss->fd, qname));
+						goto match;
+					}
+				} else {
+					SSL_DBG(("%d: DANE[%d]: %s: invalid TLSA SHA-1 RR", SSL_GETPID(), ss->fd, qname));
+				}
+				break;
+			default:
+				SSL_DBG(("%d: DANE[%d]: %s: TLSA RR with unhandled hash type %d", SSL_GETPID(), ss->fd, qname, htype));
+				break;
+			}
+			break;
+		default:
+			SSL_DBG(("%d: DANE[%d]: %s: TLSA RR with unhandled certificate type %d", SSL_GETPID(), ss->fd, qname, ctype));
+			break;
+		}
+	}
+
+	SSL_DBG(("%d: DANE[%d]: %s: cert did not match any TLSA RR, unauthorized", SSL_GETPID(), ss->fd, qname));
+	PORT_SetError(SSL_ERROR_DANE_UNAUTHORIZED);
+
+out2:
+	ub_resolve_free(res);
+out1:
+	free(qname);
+	return SECFailure;
+
+match:
+	ub_resolve_free(res);
+	free(qname);
+	return SECSuccess;
+}
diff --git a/security/nss/lib/ssl/sslerr.h b/security/nss/lib/ssl/sslerr.h
index 67b1c3c..04a1df9 100644
--- a/security/nss/lib/ssl/sslerr.h
+++ b/security/nss/lib/ssl/sslerr.h
@@ -203,6 +203,13 @@ SSL_ERROR_RX_UNEXPECTED_UNCOMPRESSED_RECORD	= (SSL_ERROR_BASE + 114),
 
 SSL_ERROR_WEAK_SERVER_EPHEMERAL_DH_KEY  = (SSL_ERROR_BASE + 115),
 
+SSL_ERROR_DANE_INIT_FAILED  = (SSL_ERROR_BASE + 116),
+SSL_ERROR_DANE_PORT_UNSET   = (SSL_ERROR_BASE + 117),
+SSL_ERROR_DANE_QUERY_FAILED = (SSL_ERROR_BASE + 118),
+SSL_ERROR_DANE_QUERY_BOGUS  = (SSL_ERROR_BASE + 119),
+SSL_ERROR_DANE_UNAUTHORIZED = (SSL_ERROR_BASE + 120),
+SSL_ERROR_DANE_INAPPLICABLE = (SSL_ERROR_BASE + 121),
+
 SSL_ERROR_END_OF_LIST	/* let the c compiler determine the value of this. */
 } SSLErrorCodes;
 #endif /* NO_SECURITY_ERROR_ENUM */
diff --git a/security/nss/lib/ssl/sslimpl.h b/security/nss/lib/ssl/sslimpl.h
index 0f019ba..d2010c8 100644
--- a/security/nss/lib/ssl/sslimpl.h
+++ b/security/nss/lib/ssl/sslimpl.h
@@ -1032,6 +1032,7 @@ struct sslSocketStr {
 
     /* protected by firstHandshakeLock AND (in ssl3) ssl3HandshakeLock. */
     const char      *url;				/* ssl 2 & 3 */
+    PRUint16         servicePort;
 
     sslHandshakeFunc handshake;				/*firstHandshakeLock*/
     sslHandshakeFunc nextHandshake;			/*firstHandshakeLock*/
@@ -1612,6 +1613,9 @@ extern SECStatus ssl_InitSessionCacheLocks(PRBool lazyInit);
 
 extern SECStatus ssl_FreeSessionCacheLocks(void);
 
+extern void ssl_DANEInit();
+extern SECStatus ssl_DANECheck(sslSocket *ss);
+
 
 /********************** misc calls *********************/
 
diff --git a/security/nss/lib/ssl/sslsecur.c b/security/nss/lib/ssl/sslsecur.c
index 3a4107c..935d33c 100644
--- a/security/nss/lib/ssl/sslsecur.c
+++ b/security/nss/lib/ssl/sslsecur.c
@@ -1317,6 +1317,32 @@ SSL_SetURL(PRFileDesc *fd, const char *url)
     return rv;
 }
 
+/* Based on SSL_SetURL ~ Matt */
+SECStatus
+SSL_SetServicePort(PRFileDesc *fd, PRUint16 servicePort)
+{
+    sslSocket *   ss = ssl_FindSocket(fd);
+    SECStatus     rv = SECSuccess;
+
+    if (!ss) {
+	SSL_DBG(("%d: SSL[%d]: bad socket in SSLSetServicePort",
+		 SSL_GETPID(), fd));
+	return SECFailure;
+    }
+    ssl_Get1stHandshakeLock(ss);
+    ssl_GetSSL3HandshakeLock(ss);
+
+    ss->servicePort = servicePort;
+    if ( ss->servicePort == 0 ) {
+	rv = SECFailure;
+    }
+
+    ssl_ReleaseSSL3HandshakeLock(ss);
+    ssl_Release1stHandshakeLock(ss);
+
+    return rv;
+}
+
 /*
  * Allow the application to pass the set of trust anchors
  */
diff --git a/security/nss/lib/ssl/sslsock.c b/security/nss/lib/ssl/sslsock.c
index 6fd9cbc..82f4580 100644
--- a/security/nss/lib/ssl/sslsock.c
+++ b/security/nss/lib/ssl/sslsock.c
@@ -2325,6 +2325,7 @@ ssl_NewSocket(PRBool makeLocks)
 	    SSL_TRACE(("SSL: requireSafeNegotiation set to %d", 
 	                PR_TRUE));
 	}
+	ssl_DANEInit();
     }
 #endif /* NSS_HAVE_GETENV */
     if (ssl_force_locks)
