Plan 9 from Bell Labs’s /usr/web/sources/contrib/mospak/tls-1.2/ip-httpd-vhosts-sni.diff

Copyright © 2021 Plan 9 Foundation.
Distributed under the MIT License.
Download the Plan 9 distribution.


--- sys/include/libsec.h
+++ sys/include/libsec.h
@@ -511,6 +511,11 @@ typedef struct TLSconn{
 	char	*serverName;	/* SNI: set by client before tlsClient; set by tlsServer to observed SNI */
 	PEMChain *rootCAchain;	/* trust anchors for X509verifychain; nil = no chain validation */
 	int	emsstrict;	/* RFC 7627 Section 5.2: refuse handshake when peer lacks extended_master_secret */
+	/* RFC 6066 Section 3 SNI vhost routing: if non-nil, libsec calls
+	 * this after parsing ClientHello server_name; callback inspects
+	 * conn->serverName and updates conn->cert/certlen/chain to match.
+	 * Returns 0 to continue, -1 to abort with unrecognized_name(112). */
+	int	(*selectcert)(struct TLSconn *conn);
 } TLSconn;
 
 /* tlshand.c */
--- sys/src/cmd/ip/httpd/httpd.c
+++ sys/src/cmd/ip/httpd/httpd.c
@@ -1,6 +1,7 @@
 #include <u.h>
 #include <libc.h>
 #include <auth.h>
+#include <bio.h>
 #include <mp.h>
 #include <libsec.h>
 #include "httpd.h"
@@ -12,6 +13,7 @@ typedef struct System		System;
 
 typedef struct Strings		Strings;
 typedef struct System		System;
+typedef struct Vhost		Vhost;
 
 struct Strings
 {
@@ -25,6 +27,18 @@ struct System {
 	ulong	last;
 	System	*next;			/* next in chain */
 };
+/*
+ * RFC 6066 Section 3 SNI vhost: one entry per HTTPS hostname.
+ * Wildcard "*" entry, if present, is the default for unmatched SNI
+ * (or no SNI at all).
+ */
+struct Vhost {
+	Vhost	*next;
+	char	*hostname;
+	uchar	*cert;
+	int	certlen;
+	PEMChain *chain;
+};
 
 char	*netdir;
 char	*HTTPLOG = "httpd/log";
@@ -42,16 +56,20 @@ static	int		notfound(HConnect *c, char *url);
 static	char*		stripprefix(char*, char*);
 static	char*		sysdom(void);
 static	int		notfound(HConnect *c, char *url);
+static	void		loadvhosts(char*);
+static	int		selectVhostCert(TLSconn*);
 
 uchar *certificate;
 int certlen;
-PEMChain *certchain;	
+PEMChain *certchain;
+static	Vhost		*vhosts;
+static	Vhost		*defaultVhost;
 
 void
 usage(void)
 {
 	fprint(2, "usage: httpd [-c certificate] [-C CAchain] [-a srvaddress] "
-		"[-d domain] [-n namespace] [-w webroot]\n");
+		"[-d domain] [-n namespace] [-v vhosts] [-w webroot]\n");
 	exits("usage");
 }
 
@@ -87,6 +105,9 @@ main(int argc, char **argv)
 	case 'd':
 		hmydomain = EARGF(usage());
 		break;
+	case 'v':
+		loadvhosts(EARGF(usage()));
+		break;
 	case 'w':
 		webroot = EARGF(usage());
 		break;
@@ -134,8 +155,17 @@ main(int argc, char **argv)
 	urlinit();
 	statsinit();
 
+	/* Refuse to start in a half-configured TLS state: named vhosts
+	 * present, no '*' default, no -c override.  Otherwise every
+	 * connection without a matching SNI would fail at handshake
+	 * setup with a cryptic factotum_rsa_open error. */
+	if(vhosts != nil && defaultVhost == nil && certificate == nil)
+		sysfatal("vhosts file has no '*' default and no -c override");
+
 	becomenone(namespace);
-	dolisten(netmkaddr(address, "tcp", certificate == nil ? "http" : "https"));
+	dolisten(netmkaddr(address, "tcp",
+		certificate == nil && vhosts == nil && defaultVhost == nil
+		? "http" : "https"));
 	exits(nil);
 }
 
@@ -304,12 +334,27 @@ dolisten(char *address)
 			 *  see if we know the service requested
 			 */
 			data = accept(ctl, ndir);
-			if(data >= 0 && certificate != nil){
+			if(data >= 0 && (certificate != nil
+			|| vhosts != nil || defaultVhost != nil)){
 				memset(&conn, 0, sizeof(conn));
-				conn.cert = certificate;
-				conn.certlen = certlen;
-				if (certchain != nil)
-					conn.chain = certchain;
+				/*
+				 * Initial cert: -c override wins; otherwise the
+				 * wildcard vhost is the default.  The selectcert
+				 * callback (RFC 6066 SNI) may swap to a per-vhost
+				 * cert once the ClientHello arrives.
+				 */
+				if(certificate != nil){
+					conn.cert = certificate;
+					conn.certlen = certlen;
+					if(certchain != nil)
+						conn.chain = certchain;
+				}else if(defaultVhost != nil){
+					conn.cert = defaultVhost->cert;
+					conn.certlen = defaultVhost->certlen;
+					conn.chain = defaultVhost->chain;
+				}
+				if(vhosts != nil || defaultVhost != nil)
+					conn.selectcert = selectVhostCert;
 				data = tlsServer(data, &conn);
 				scheme = "https";
 			}else
@@ -656,6 +701,81 @@ sysdom(void)
 	if(dn == nil)
 		dn = "who cares";
 	return dn;
+}
+
+/*
+ * Load /sys/lib/httpd/vhosts (or path passed via -v).  Each non-comment
+ * line is "hostname certfile keyfile"; the keyfile is opened as a
+ * cert chain (the file may contain extra intermediates).  A wildcard
+ * "*" hostname is the default for unmatched SNI.  RFC 6066 Section 3.
+ */
+static void
+loadvhosts(char *path)
+{
+	Biobuf *b;
+	char *line, *field[3];
+	Vhost *v;
+
+	b = Bopen(path, OREAD);
+	if(b == nil)
+		sysfatal("cannot open vhosts %s: %r", path);
+	while((line = Brdline(b, '\n')) != nil){
+		line[Blinelen(b)-1] = 0;
+		while(*line == ' ' || *line == '\t')
+			line++;
+		if(*line == '#' || *line == 0)
+			continue;
+		if(tokenize(line, field, nelem(field)) != 3)
+			continue;
+		v = mallocz(sizeof *v, 1);
+		if(v == nil)
+			sysfatal("vhost: out of memory");
+		v->hostname = estrdup(field[0]);
+		v->cert = readcert(field[1], &v->certlen);
+		if(v->cert == nil)
+			sysfatal("vhost %s: cannot read cert %s: %r",
+				field[0], field[1]);
+		v->chain = readcertchain(field[2]);
+		if(strcmp(v->hostname, "*") == 0){
+			defaultVhost = v;
+		}else{
+			v->next = vhosts;
+			vhosts = v;
+		}
+	}
+	Bterm(b);
+}
+
+/*
+ * RFC 6066 SNI vhost dispatch: walk the configured vhosts and pick the
+ * cert whose hostname matches the SNI exactly (case-insensitive).  Fall
+ * back to the wildcard "*" entry if present.  Wildcard cert subject
+ * matching (e.g., *.example.com) is the cert's own job, not ours.
+ */
+static int
+selectVhostCert(TLSconn *conn)
+{
+	Vhost *v;
+
+	if(conn->serverName != nil){
+		for(v = vhosts; v != nil; v = v->next){
+			if(cistrcmp(v->hostname, conn->serverName) == 0){
+				conn->cert = v->cert;
+				conn->certlen = v->certlen;
+				conn->chain = v->chain;
+				return 0;
+			}
+		}
+	}
+	if(defaultVhost != nil){
+		conn->cert = defaultVhost->cert;
+		conn->certlen = defaultVhost->certlen;
+		conn->chain = defaultVhost->chain;
+		return 0;
+	}
+	werrstr("no vhost matches SNI %s",
+		conn->serverName != nil ? conn->serverName : "(none)");
+	return -1;
 }
 
 /*
--- sys/src/libsec/port/tlshand.c
+++ sys/src/libsec/port/tlshand.c
@@ -227,6 +227,7 @@ enum {
 	EUserCanceled = 90,
 	ENoRenegotiation = 100,
 	EUnsupportedExtension = 110,	/* RFC 5246 Section 7.4.1.4 */
+	EUnrecognizedName = 112,	/* RFC 6066 Section 3 */
 	EMax = 256
 };
 
@@ -380,7 +381,7 @@ static uchar ecPointFormats[] = {
 	0,	/* uncompressed */
 };
 
-static TlsConnection *tlsServer2(int ctl, int hand, uchar *cert, int ncert, int emsstrict, int (*trace)(char*fmt, ...), PEMChain *chain);
+static TlsConnection *tlsServer2(int ctl, int hand, TLSconn *conn);
 static TlsConnection *tlsClient2(int ctl, int hand, uchar *csid, int ncsid,
 	char *serverName, int emsstrict, PEMChain *rootCA,
 	int (*trace)(char*fmt, ...));
@@ -517,7 +518,7 @@ tlsServer(int fd, TLSconn *conn)
 		return -1;
 	}
 	fprint(ctl, "fd %d 0x%x", fd, ProtocolVersion);
-	tls = tlsServer2(ctl, hand, conn->cert, conn->certlen, conn->emsstrict, conn->trace, conn->chain);
+	tls = tlsServer2(ctl, hand, conn);
 	snprint(dname, sizeof dname, "#a/tls/%s/data", buf);
 	data = open(dname, ORDWR);
 	close(fd);
@@ -612,16 +613,22 @@ static TlsConnection *
 }
 
 static TlsConnection *
-tlsServer2(int ctl, int hand, uchar *cert, int ncert,
-	int emsstrict, int (*trace)(char*fmt, ...), PEMChain *chp)
+tlsServer2(int ctl, int hand, TLSconn *conn)
 {
 	TlsConnection *c;
 	Msg m;
 	Bytes *csid;
 	uchar sid[SidSize], kd[MaxKeyData];
+	uchar *cert;
+	PEMChain *chp;
+	int (*trace)(char*fmt, ...);
 	char *secrets;
-	int cipher, compressor, nsid, rv, numcerts, i;
+	int cipher, compressor, nsid, rv, numcerts, i, ncert;
 
+	cert = conn->cert;
+	ncert = conn->certlen;
+	chp = conn->chain;
+	trace = conn->trace;
 	if(trace)
 		trace("tlsServer2\n");
 	if(!initCiphers())
@@ -631,7 +638,7 @@ tlsServer2(int ctl, int hand, uchar *cert, int ncert,
 	c->hand = hand;
 	c->trace = trace;
 	c->version = ProtocolVersion;
-	c->emsStrict = emsstrict;
+	c->emsStrict = conn->emsstrict;
 
 	memset(&m, 0, sizeof(m));
 	if(!msgRecv(c, &m)){
@@ -747,6 +754,26 @@ tlsServer2(int ctl, int hand, uchar *cert, int ncert,
 			tlsError(c, EDecodeError, "malformed clientHello extension");
 			goto Err;
 		}
+	}
+
+	/*
+	 * RFC 6066 Section 3 vhost cert routing.  c->sni is the client's
+	 * presented host_name; mirror it into conn->serverName and let the
+	 * caller pick a matching cert.  The callback may replace
+	 * conn->cert/certlen/chain; refresh our locals so the Certificate
+	 * message and signing key match what was selected.
+	 */
+	if(c->sni != nil && conn->selectcert != nil){
+		free(conn->serverName);
+		conn->serverName = strdup(c->sni);
+		if(conn->selectcert(conn) < 0){
+			tlsError(c, EUnrecognizedName,
+				"RFC 6066: no vhost matches SNI");
+			goto Err;
+		}
+		cert = conn->cert;
+		ncert = conn->certlen;
+		chp = conn->chain;
 	}
 
 	csid = m.u.clientHello.sid;

Bell Labs OSI certified Powered by Plan 9

(Return to Plan 9 Home Page)

Copyright © 2021 Plan 9 Foundation. All Rights Reserved.
Comments to webmaster@9p.io.