--- 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;
|