Plan9/tlssrv(8) with Server Name Indication (SNI) support
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.
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);