⑨ lab ≡ ByteLabs

Plan9/tlssrv(8) with Server Name Indication (SNI) support

— Igor Böhm

One IP to serve them (i.e. SSL certificates) all! A description of a variant of tlssrv.sni(8) supporting the Server Name Indication (SNI) extension enabling one to use multiple SSL certificates with a single IP address.

⑨ lab ≡ website…

Reactor served via tlssrv.sni(8) running on 9front

Installation

% git/clone git://shithub.us/igor/tlssrv.sni
% cd tlssrv.sni
% mk install

Example Setup

The easiest way to fetch and renew a TLS certificate on 9front is via acmed(8). We are going to request 4 SSL certificates, that is 9lab.org, mux.9lab.org, bytelabs.org, and mux.bytelabs.org, backed by the same IP address.

Initially an account key must be generated (see acmed(8)):

% ramfs -p ; cd /tmp
% auth/rsagen -t \
    'service=acme role=sign hash=sha256 acct=igor@9lab.org' \
    >account.key
% auth/rsa2jwk account.key \
    >/sys/lib/tls/acmed/igor@9lab.org.pub

The account.key must be loaded into factotum(4); however, it is best to store it in secstore(1) instead of storing it unencrypted on the file system:

% auth/secstore -g factotum
secstore password:
% cat account.key >> factotum
% auth/secstore -p factotum
secstore password:
% read -m factotum > /mnt/factotum/ctl

Please consult secstore(1) as well as the excellent 9front FQA secstore documentation if the above doesn’t make sense or doesn’t work for you.

Next, generate a rsa(8) key (i.e. certificate.key) and store it in secstore(1):

% auth/rsagen -t 'service=tls role=client owner=*' \
    >certificate.key
% auth/secstore -g factotum
secstore password:
% cat certificate.key >> factotum
% auth/secstore -p factotum
secstore password:
% read -m factotum > /mnt/factotum/ctl

See rsa(8) and tlssrv(8) for more examples on how to use RSA keys.

Now it is time to create certificate signing requests (i.e. mux.9lab.org.csr):

% auth/rsa2csr 'CN=9lab.org' certificate.key \
    >/sys/lib/tls/acmed/9lab.org.csr
% auth/rsa2csr 'CN=mux.9lab.org' certificate.key \
    >/sys/lib/tls/acmed/mux.9lab.org.csr
% auth/rsa2csr 'CN=bytelabs.org' certificate.key \
    >/sys/lib/tls/acmed/bytelabs.org.csr
% auth/rsa2csr 'CN=mux.bytelabs.org' certificate.key \
    >/sys/lib/tls/acmed/mux.bytelabs.org.csr

Finally, the certificates for your domains can be fetched. This requires webfs(4) to be mounted as the ACME protocol uses HTTP to talk to the provider.

% webfs
% auth/acmed igor@9lab.org /sys/lib/tls/acmed/9lab.org.csr \
    >/sys/lib/tls/acmed/9lab.org.crt
% auth/acmed igor@9lab.org /sys/lib/tls/acmed/mux.9lab.org.csr \
    >/sys/lib/tls/acmed/mux.9lab.org.crt
% auth/acmed igor@9lab.org /sys/lib/tls/acmed/bytelabs.org.csr \
    >/sys/lib/tls/acmed/bytelabs.org.crt
% auth/acmed igor@9lab.org /sys/lib/tls/acmed/mux.bytelabs.org.csr \
    >/sys/lib/tls/acmed/mux.bytelabs.org.crt

The above incantation is also used to renew certificates. The following is handy to display a certificate:

% auth/pemdecode 'CERTIFICATE' /sys/lib/tls/acmed/mux.9lab.org.crt | auth/x5092pub
key proto=rsa size=2048 ek=… n=… subject=mux.9lab.org

At last, to listen(8) on https port 443 for requests modify the file /bin/service/tcp443 as follows:

% cat /bin/service/tcp443
#!/bin/rc
exec tlssrv -c/sys/lib/tls/acmed/9lab.org.crt -ltcp80 -r`{cat $3/remote} /bin/tcp80 `{cat $3/remote}>>[2]/sys/log/tcp80

If you already have /bin/service/tcp443 with a certificate setup you do not need any additional steps to enable SNI to work other than ensuring your certificates are in the appropriate folder /sys/lib/tls/acmed/ using the right suffix (see Caveats section) and pointing tlssrv to the

Caveats

A main or fallback certificate can be specified to tlssrv(8) via the -c option. If a Server Name Identifier SNI is provided, we attempt to load its certificate from:

  snprint(path, sizeof(path), "/sys/lib/tls/acmed/%s.crt", c->serverName);

That means the certificate has to be present in /sys/lib/tls/acmed/ with the name SNI.crt, where SNI is the the server name indicator provided by the client.

Diff

% diff -u /sys/src/libsec/port/tlshand.c tlssrv.sni/tlshand.c
--- /sys/src/libsec/port/tlshand.c
+++ tlssrv.sni/tlshand.c
@@ -98,6 +98,8 @@
 	char *digest;	// name of digest algorithm to use
 	char *enc;	// name of encryption algorithm to use
 
+	char *serverName;	// server name indication; extension
+
 	// for finished messages
 	HandshakeHash	handhash;
 	Finished	finished;
@@ -355,7 +357,7 @@
 };
 
 static TlsConnection *tlsServer2(int ctl, int hand,
-	uchar *cert, int certlen,
+	uchar **cert, int certlen,
 	char *pskid, uchar *psk, int psklen,
 	int (*trace)(char*fmt, ...), PEMChain *chain);
 static TlsConnection *tlsClient2(int ctl, int hand,
@@ -456,7 +458,7 @@
 	data = -1;
 	fprint(ctl, "fd %d 0x%x", fd, ProtocolVersion);
 	tls = tlsServer2(ctl, hand,
-		conn->cert, conn->certlen,
+		&(conn->cert), conn->certlen,
 		conn->pskID, conn->psk, conn->psklen,
 		conn->trace, conn->chain);
 	if(tls != nil){
@@ -659,9 +661,23 @@
 		if(e-p < 4)
 			goto Short;
 		p += 4;
-		if(e-p < (n = get16(p-2)))
+		if(e-p < (n = get16(p-2)))	/* Length */
 			goto Short;
-		switch(get16(p-4)){
+		switch(get16(p-4)){			/* Type */
+		case Extsni:
+			if(n < 4 || get16(p) != (n -= 2))
+				goto Short;
+			if(*(p+2) != 0)			/* Server Name Type: host_name */
+				break;
+			p += 2+1+2;
+			if(e-p < (n = get16(p-2)))
+				goto Short;
+			if(n > 255)				/* DNS name can not exceed 255 bytes RFC1035 */
+				break;
+			c->serverName = emalloc(n+1);
+			memmove(c->serverName, p, n);
+			c->serverName[n] = 0;
+			break;
 		case Extec:
 			if(n < 4 || n % 2 || get16(p) != (n -= 2))
 				goto Short;
@@ -717,7 +733,7 @@
 
 static TlsConnection *
 tlsServer2(int ctl, int hand,
-	uchar *cert, int certlen,
+	uchar **cert, int certlen,
 	char *pskid, uchar *psk, int psklen,
 	int (*trace)(char*fmt, ...), PEMChain *chp)
 {
@@ -765,9 +781,26 @@
 		c->sec->psk = psk;
 		c->sec->psklen = psklen;
 	}
+	if(checkClientExtensions(c, m.u.clientHello.extensions) < 0)
+		goto Err;
 	if(certlen > 0){
+		/* override default certificate using Server Name Indication (SNI) extension */
+		if(c->serverName){
+			char path[512];
+			PEMChain *chain;
+
+			snprint(path, sizeof(path), "/sys/lib/tls/acmed/%s.crt", c->serverName);
+			if(trace)
+				trace("ClientHello extension server name identifier selects %s\n", path);
+			chain = readcertchain(path);
+			if (chain){
+				free(*cert);
+				*cert = chain->pem;
+				certlen = chain->pemlen;
+			}
+		}
 		/* server certificate */
-		c->sec->rsapub = X509toRSApub(cert, certlen, nil, 0);
+		c->sec->rsapub = X509toRSApub(*cert, certlen, nil, 0);
 		if(c->sec->rsapub == nil){
 			tlsError(c, EHandshakeFailure, "invalid X509/rsa certificate");
 			goto Err;
@@ -780,8 +813,6 @@
 	}
 	if(lookupid(m.u.clientHello.ciphers, TLS_EMPTY_RENEGOTIATION_INFO_SCSV) >= 0)
 		c->sec->reneg = 1;
-	if(checkClientExtensions(c, m.u.clientHello.extensions) < 0)
-		goto Err;
 	cipher = okCipher(m.u.clientHello.ciphers, psklen > 0, c->sec->nc != nil);
 	if(cipher < 0 || !setAlgs(c, cipher)) {
 		tlsError(c, EHandshakeFailure, "no matching cipher suite");
@@ -813,7 +844,7 @@
 		numcerts = countchain(chp);
 		m.u.certificate.ncert = 1 + numcerts;
 		m.u.certificate.certs = emalloc(m.u.certificate.ncert * sizeof(Bytes*));
-		m.u.certificate.certs[0] = makebytes(cert, certlen);
+		m.u.certificate.certs[0] = makebytes(*cert, certlen);
 		for (i = 0; i < numcerts && chp; i++, chp = chp->next)
 			m.u.certificate.certs[i+1] = makebytes(chp->pem, chp->pemlen);
 		if(!msgSend(c, &m, AQueue))
@@ -2113,6 +2144,8 @@
 	factotum_rsa_close(c->sec->rpc);
 	rsapubfree(c->sec->rsapub);
 	freebytes(c->cert);
+
+	free(c->serverName);
 
 	memset(c, 0, sizeof(*c));
 	free(c);

#Plan9 #9front