--- sys/src/libsec/port/tlshand.c
+++ sys/src/libsec/port/tlshand.c
@@ -90,6 +90,14 @@ typedef struct TlsConnection{
Finished finished;
int serverSignFlags; /* AlgRsaSign|AlgEcSign; gates okCipher */
int serverCurve; /* negotiated ECDHE group, 0 = none */
+ char *sni; /* RFC 6066 server-side observed SNI */
+ int extSniSeen; /* client offered SNI; gates server_name ack */
+ int extEcPointFormatsSeen; /* client offered ext; gates ack */
+ Ints *peerSigAlgs; /* RFC 5246 7.4.1.4.1 sigalgs from peer */
+ /* Client-side: extension types we advertised, for unsupported_extension(110)
+ * detection on ServerHello (RFC 5246 Section 7.4.1.4). */
+ int nClientOffered;
+ int clientOffered[16];
} TlsConnection;
typedef struct Msg{
@@ -206,6 +214,7 @@ enum {
EInternalError = 80,
EUserCanceled = 90,
ENoRenegotiation = 100,
+ EUnsupportedExtension = 110, /* RFC 5246 Section 7.4.1.4 */
EMax = 256
};
@@ -428,11 +437,41 @@ static Bytes* findExtension(Bytes *ext, int type);
static int isECDHE(int tlsid);
static Bytes* findExtension(Bytes *ext, int type);
-static Bytes* clientHelloExtensions(char *serverName);
+static Bytes* clientHelloExtensions(TlsConnection *c, char *serverName);
static uchar* tlsSecECDHEc(TlsSec *sec, Bytes *Ys, int *npub);
static uchar* tlsSecECDHEcNist(TlsSec *sec, void (*curveinit)(mpint*,mpint*,mpint*,mpint*,mpint*,mpint*,mpint*), Bytes *Ys, int *npub);
static char* verifyDHparams(TlsSec *sec, Msg *m, Bytes *cert);
+/*
+ * Extension framework (RFC 5246 Section 7.4.1.4, RFC 8446 Section 4.2).
+ * Each ExtAlg entry pairs a wire type with a build callback (emits the
+ * payload, w/o the type+length header) and a parse callback (consumes a
+ * received payload). Builders that decline to advertise this connection
+ * return -1; the dispatch helper skips them. Parsers return 0 on
+ * success, -1 on malformed payload.
+ *
+ * Per-direction tables (clientHelloBuilders, clientHelloParsers,
+ * serverHelloBuilders, serverHelloParsers) and their dispatch wrappers
+ * are defined together near the bottom of this file. Adding a new
+ * extension means adding one entry to the relevant table plus the
+ * matching build/parse function -- no other plumbing.
+ */
+typedef struct ExtAlg ExtAlg;
+typedef struct ExtCtx ExtCtx;
+struct ExtCtx {
+ char *serverName; /* SNI; nil on server, may be nil on client */
+};
+struct ExtAlg {
+ int type;
+ int (*build)(TlsConnection*, ExtCtx*, uchar*, int);
+ int (*parse)(TlsConnection*, ExtCtx*, uchar*, int);
+};
+
+static int clientHelloBuildExt(TlsConnection *c, ExtCtx *ctx, uchar *out, int outsize);
+static int clientHelloParseExt(TlsConnection *c, ExtCtx *ctx, uchar *in, int inlen);
+static int serverHelloBuildExt(TlsConnection *c, ExtCtx *ctx, uchar *out, int outsize);
+static int serverHelloParseExt(TlsConnection *c, ExtCtx *ctx, uchar *in, int inlen);
+
//================= client/server ========================
// push TLS onto fd, returning new (application) file descriptor
@@ -672,6 +711,20 @@ tlsServer2(int ctl, int hand, uchar *cert, int ncert,
goto Err;
}
+ /* Walk client's offered extensions through the parser table.
+ * Unknown types are silently skipped per RFC 5246 Section 7.4.1.4. */
+ if(m.u.clientHello.extensions != nil){
+ ExtCtx ctx;
+
+ memset(&ctx, 0, sizeof ctx);
+ if(clientHelloParseExt(c, &ctx,
+ m.u.clientHello.extensions->data,
+ m.u.clientHello.extensions->len) < 0){
+ tlsError(c, EDecodeError, "malformed clientHello extension");
+ goto Err;
+ }
+ }
+
csid = m.u.clientHello.sid;
if(trace)
trace(" cipher %d, compressor %d, csidlen %d\n", cipher, compressor, csid->len);
@@ -695,6 +748,16 @@ tlsServer2(int ctl, int hand, uchar *cert, int ncert,
m.u.serverHello.compressor = compressor;
c->sid = makebytes(sid, nsid);
m.u.serverHello.sid = makebytes(c->sid->data, c->sid->len);
+ {
+ uchar extbuf[512];
+ ExtCtx ctx;
+ int extlen;
+
+ memset(&ctx, 0, sizeof ctx);
+ extlen = serverHelloBuildExt(c, &ctx, extbuf, sizeof extbuf);
+ if(extlen > 0)
+ m.u.serverHello.extensions = makebytes(extbuf, extlen);
+ }
if(!msgSend(c, &m, AQueue))
goto Err;
msgClear(&m);
@@ -883,7 +946,7 @@ tlsClient2(int ctl, int hand, uchar *csid, int ncsid,
m.u.clientHello.ciphers = makeciphers();
m.u.clientHello.compressors = makebytes(compressors,sizeof(compressors));
if(c->clientVersion >= TLS12Version)
- m.u.clientHello.extensions = clientHelloExtensions(serverName);
+ m.u.clientHello.extensions = clientHelloExtensions(c, serverName);
if(!msgSend(c, &m, AFlush))
goto Err;
msgClear(&m);
@@ -914,6 +977,18 @@ tlsClient2(int ctl, int hand, uchar *csid, int ncsid,
tlsError(c, EIllegalParameter, "invalid compression");
goto Err;
}
+ if(m.u.serverHello.extensions != nil){
+ ExtCtx ctx;
+
+ memset(&ctx, 0, sizeof ctx);
+ ctx.serverName = serverName;
+ if(serverHelloParseExt(c, &ctx,
+ m.u.serverHello.extensions->data,
+ m.u.serverHello.extensions->len) < 0){
+ tlsError(c, EDecodeError, "malformed serverHello extension");
+ goto Err;
+ }
+ }
dhx = isECDHE(cipher);
msgClear(&m);
@@ -1929,6 +2004,8 @@ tlsConnectionFree(TlsConnection *c)
tlsSecClose(c->sec);
freebytes(c->sid);
freebytes(c->cert);
+ free(c->sni);
+ freeints(c->peerSigAlgs);
memset(c, 0, sizeof *c);
free(c);
}
@@ -2454,52 +2531,390 @@ findExtension(Bytes *ext, int type)
/*
* Build the ClientHello extensions blob (inner form, without the outer
- * 2-byte length). Contains: server_name (when given, RFC 6066 Section 3),
- * supported_groups = [X25519], ec_point_formats = [uncompressed],
- * signature_algorithms.
+ * 2-byte length). Wire bytes are produced by the table-driven
+ * clientHelloBuildExt; this is a thin Bytes* wrapper for the call site
+ * in tlsClient2.
*/
static Bytes*
-clientHelloExtensions(char *serverName)
+clientHelloExtensions(TlsConnection *c, char *serverName)
{
- uchar buf[512], *p;
- int n, snlen, i;
+ uchar buf[512];
+ ExtCtx ctx;
+ int n;
- p = buf;
+ memset(&ctx, 0, sizeof ctx);
+ ctx.serverName = serverName;
+ n = clientHelloBuildExt(c, &ctx, buf, sizeof buf);
+ if(n <= 0)
+ return nil;
+ return makebytes(buf, n);
+}
- if(serverName != nil && *serverName != 0){
- snlen = strlen(serverName);
- put16(p, ExtSni);
- put16(p+2, snlen + 5); /* ext content = list_len(2) + entry(1+2+snlen) */
- put16(p+4, snlen + 3); /* server_name_list length */
- p[6] = 0; /* NameType = host_name */
- put16(p+7, snlen);
- memmove(p+9, serverName, snlen);
- p += 9 + snlen;
- }
+/*
+ * SNI (RFC 6066 Section 3): server_name_list with one host_name entry.
+ * Declined when serverName is unset.
+ */
+static int
+buildSni(TlsConnection *c, ExtCtx *ctx, uchar *out, int outsize)
+{
+ int snlen;
- put16(p, ExtSupportedGroups);
- put16(p+2, sizeof(supportedGroups) + 2);
- put16(p+4, sizeof(supportedGroups));
- memmove(p+6, supportedGroups, sizeof(supportedGroups));
- p += 6 + sizeof(supportedGroups);
+ USED(c);
+ if(ctx->serverName == nil || *ctx->serverName == 0)
+ return -1;
+ snlen = strlen(ctx->serverName);
+ if(snlen + 5 > outsize)
+ return -1;
+ put16(out, snlen + 3); /* server_name_list length */
+ out[2] = 0; /* NameType = host_name */
+ put16(out+3, snlen);
+ memmove(out+5, ctx->serverName, snlen);
+ return 5 + snlen;
+}
- put16(p, ExtEcPointFormats);
- put16(p+2, sizeof(ecPointFormats) + 1);
- p[4] = sizeof(ecPointFormats);
- memmove(p+5, ecPointFormats, sizeof(ecPointFormats));
- p += 5 + sizeof(ecPointFormats);
+/*
+ * supported_groups (RFC 8422 Section 5.1.1): named-curve list we offer.
+ */
+static int
+buildSupportedGroups(TlsConnection *c, ExtCtx *ctx, uchar *out, int outsize)
+{
+ USED(c); USED(ctx);
+ if((int)(sizeof(supportedGroups) + 2) > outsize)
+ return -1;
+ put16(out, sizeof(supportedGroups));
+ memmove(out+2, supportedGroups, sizeof(supportedGroups));
+ return 2 + sizeof(supportedGroups);
+}
+/*
+ * ec_point_formats (RFC 4492 Section 5.1.2): uncompressed only.
+ */
+static int
+buildEcPointFormats(TlsConnection *c, ExtCtx *ctx, uchar *out, int outsize)
+{
+ USED(c); USED(ctx);
+ if((int)(sizeof(ecPointFormats) + 1) > outsize)
+ return -1;
+ out[0] = sizeof(ecPointFormats);
+ memmove(out+1, ecPointFormats, sizeof(ecPointFormats));
+ return 1 + sizeof(ecPointFormats);
+}
+
+/*
+ * signature_algorithms (RFC 5246 Section 7.4.1.4.1).
+ */
+static int
+buildSigalgs(TlsConnection *c, ExtCtx *ctx, uchar *out, int outsize)
+{
+ int i, n;
+
+ USED(c); USED(ctx);
n = nelem(sigAlgs);
- put16(p, ExtSigalgs);
- put16(p+2, 2*n + 2);
- put16(p+4, 2*n);
- p += 6;
+ if(2*n + 2 > outsize)
+ return -1;
+ put16(out, 2*n);
+ for(i = 0; i < n; i++)
+ put16(out + 2 + 2*i, sigAlgs[i]);
+ return 2 + 2*n;
+}
+
+/*
+ * server_name parser, server-side (RFC 6066 Section 3). Records the
+ * presented host_name on c->sni and sets c->extSniSeen so the SNI ack
+ * builder echoes the empty extension. Unknown name_types are silently
+ * ignored per the RFC.
+ */
+static int
+parseSniServer(TlsConnection *c, ExtCtx *ctx, uchar *in, int inlen)
+{
+ int listlen, namelen;
+
+ USED(ctx);
+ if(inlen < 5)
+ return -1;
+ listlen = get16(in);
+ if(listlen + 2 != inlen)
+ return -1;
+ if(in[2] != 0) /* host_name == 0; unknown name_type */
+ return 0;
+ namelen = get16(in + 3);
+ /* RFC 6066 Section 3: ServerNameList carries one entry per name_type;
+ * reject trailing data as malformed framing. Do this before alloc. */
+ if(listlen != namelen + 3)
+ return -1;
+ free(c->sni);
+ c->sni = emalloc(namelen + 1);
+ memmove(c->sni, in + 5, namelen);
+ c->sni[namelen] = 0;
+ c->extSniSeen = 1;
+ return 0;
+}
+
+/*
+ * ec_point_formats parser, server-side (RFC 4492 Section 5.1.2). We do
+ * not store the formats themselves; we only record that the client
+ * offered the extension so the ack builder can echo uncompressed when
+ * an ECDHE suite is selected.
+ */
+static int
+parseEcPointFormatsServer(TlsConnection *c, ExtCtx *ctx,
+ uchar *in, int inlen)
+{
+ int listlen, i, hasUncompressed;
+
+ USED(ctx);
+ if(inlen < 1)
+ return -1;
+ listlen = in[0];
+ if(listlen < 1 || listlen + 1 != inlen)
+ return -1;
+ /*
+ * RFC 4492 Section 5.1.2.
+ * Reject if the client's list lacks pointFormatUncompressed (0x00);
+ * we only emit uncompressed SKE points so a strictly-non-uncompressed
+ * client cannot complete an ECDHE handshake with us.
+ */
+ hasUncompressed = 0;
+ for(i = 0; i < listlen; i++)
+ if(in[1 + i] == 0)
+ hasUncompressed = 1;
+ if(!hasUncompressed)
+ return -1;
+ c->extEcPointFormatsSeen = 1;
+ return 0;
+}
+
+/*
+ * signature_algorithms parser, server-side (RFC 5246 Section 7.4.1.4.1,
+ * RFC 9155). Stash the offered SignatureAndHashAlgorithm list on
+ * c->peerSigAlgs so the SKE-sign path can pick a hash the client will
+ * accept. signDHparams hardcodes rsa_pkcs1_sha256; this parser
+ * stores the offered list for the defense-in-depth check below and
+ * for any future sigalg picker.
+ */
+static int
+parseSigalgsServer(TlsConnection *c, ExtCtx *ctx, uchar *in, int inlen)
+{
+ int listlen, n, i, hasRsaSha256;
+
+ USED(ctx);
+ if(inlen < 2)
+ return -1;
+ listlen = get16(in);
+ if(listlen < 2 || (listlen & 1) != 0 || listlen + 2 != inlen)
+ return -1;
+ n = listlen / 2;
+ freeints(c->peerSigAlgs);
+ c->peerSigAlgs = newints(n);
+ hasRsaSha256 = 0;
for(i = 0; i < n; i++){
- put16(p, sigAlgs[i]);
- p += 2;
+ c->peerSigAlgs->data[i] = get16(in + 2 + 2*i);
+ if(c->peerSigAlgs->data[i] == RSA_PKCS1_SHA256)
+ hasRsaSha256 = 1;
}
+ /*
+ * signDHparams hardcodes rsa_pkcs1_sha256. If the client
+ * advertised sigalgs but omitted RSA_PKCS1_SHA256, we have
+ * nothing to sign with -- handshake_failure rather than a
+ * (cipher, sigalg) mismatch the peer would also reject.
+ */
+ if(!hasRsaSha256)
+ return -1;
+ return 0;
+}
- return makebytes(buf, p - buf);
+/*
+ * server_name ack (RFC 6066 Section 3): the server MUST send an empty
+ * server_name extension in ServerHello iff it acted on the SNI from
+ * ClientHello. Otherwise the entry is declined.
+ */
+static int
+buildSniAck(TlsConnection *c, ExtCtx *ctx, uchar *out, int outsize)
+{
+ USED(ctx); USED(out); USED(outsize);
+ if(!c->extSniSeen)
+ return -1; /* skip extension entirely */
+ return 0; /* zero-length payload */
+}
+
+/*
+ * ec_point_formats ack (RFC 4492 Section 5.1.2): the server echoes
+ * uncompressed when an ECDHE suite is selected AND the client offered
+ * the extension. Some older clients refuse the handshake if not
+ * present; this is interop, not just spec compliance.
+ */
+static int
+buildEcPointFormatsAck(TlsConnection *c, ExtCtx *ctx,
+ uchar *out, int outsize)
+{
+ USED(ctx);
+ if(!c->extEcPointFormatsSeen || !isECDHE(c->cipher))
+ return -1;
+ if(outsize < 2)
+ return -1;
+ out[0] = 1; /* list length: one format follows */
+ out[1] = 0; /* pointFormatUncompressed */
+ return 2;
+}
+
+static ExtAlg clientHelloBuilders[] = {
+ { ExtSni, buildSni, nil },
+ { ExtSupportedGroups, buildSupportedGroups, nil },
+ { ExtEcPointFormats, buildEcPointFormats, nil },
+ { ExtSigalgs, buildSigalgs, nil },
+};
+static ExtAlg clientHelloParsers[] = {
+ { ExtSni, nil, parseSniServer },
+ { ExtEcPointFormats, nil, parseEcPointFormatsServer },
+ { ExtSigalgs, nil, parseSigalgsServer },
+};
+static ExtAlg serverHelloBuilders[] = {
+ { ExtSni, buildSniAck, nil },
+ { ExtEcPointFormats, buildEcPointFormatsAck, nil },
+};
+/*
+ * serverHelloParsers is empty in this baseline -- the client side has
+ * no extensions that require state beyond what's already validated by
+ * the framing code in parseExtensionBlock. When a future patch needs
+ * to consume a ServerHello extension (e.g., extended_master_secret,
+ * RFC 7627), it adds an entry here plus the matching parse function.
+ */
+static ExtAlg serverHelloParsers[1]; /* unused; nServerHelloParsers == 0 */
+
+/*
+ * Walk the table; for each entry that wants to be sent emit
+ * (type, length, payload). Returns total bytes written, or -1 on
+ * buffer overflow.
+ */
+static int
+buildExtensionBlock(TlsConnection *c, ExtCtx *ctx, ExtAlg *table,
+ int tableLen, uchar *out, int outsize)
+{
+ uchar *p, *e;
+ int i, n;
+
+ p = out;
+ e = out + outsize;
+ for(i = 0; i < tableLen; i++){
+ if(table[i].build == nil)
+ continue;
+ if(p + 4 > e)
+ return -1;
+ n = (*table[i].build)(c, ctx, p + 4, e - (p + 4));
+ if(n < 0)
+ continue; /* declined */
+ put16(p, table[i].type);
+ put16(p+2, n);
+ p += 4 + n;
+ /* RFC 5246 Section 7.4.1.4: client tracks advertised types so
+ * the ServerHello parser can reject unsolicited extensions.
+ * Server-side path (no c on ClientHello build) is a no-op. */
+ if(c != nil && c->isClient
+ && c->nClientOffered < (int)nelem(c->clientOffered))
+ c->clientOffered[c->nClientOffered++] = table[i].type;
+ }
+ return p - out;
+}
+
+/*
+ * Walk a received extension block (sequence of type|length|payload
+ * triples). For each known type call its parse(); unknown types are
+ * silently skipped (RFC 5246 Section 7.4.1.4). Returns 0 on success,
+ * -1 on malformed framing (caller should abort).
+ */
+static int
+parseExtensionBlock(TlsConnection *c, ExtCtx *ctx, ExtAlg *table,
+ int tableLen, uchar *in, int inlen)
+{
+ uchar *p, *e;
+ int i, t, nn, nseen;
+ int seen[32]; /* RFC 5246 Section 7.4.1.4: types must be unique */
+
+ p = in;
+ e = in + inlen;
+ nseen = 0;
+ while(p + 4 <= e){
+ t = get16(p);
+ nn = get16(p+2);
+ p += 4;
+ if(p + nn > e)
+ return -1;
+ for(i = 0; i < nseen; i++)
+ if(seen[i] == t)
+ return -1;
+ if(nseen >= (int)nelem(seen))
+ return -1;
+ seen[nseen++] = t;
+ for(i = 0; i < tableLen; i++){
+ if(table[i].type == t && table[i].parse != nil){
+ if((*table[i].parse)(c, ctx, p, nn) < 0)
+ return -1;
+ break;
+ }
+ }
+ p += nn;
+ }
+ if(p != e)
+ return -1;
+ return 0;
+}
+
+static int
+clientHelloBuildExt(TlsConnection *c, ExtCtx *ctx, uchar *out, int outsize)
+{
+ return buildExtensionBlock(c, ctx, clientHelloBuilders,
+ nelem(clientHelloBuilders), out, outsize);
+}
+
+static int
+clientHelloParseExt(TlsConnection *c, ExtCtx *ctx, uchar *in, int inlen)
+{
+ return parseExtensionBlock(c, ctx, clientHelloParsers,
+ nelem(clientHelloParsers), in, inlen);
+}
+
+static int
+serverHelloBuildExt(TlsConnection *c, ExtCtx *ctx, uchar *out, int outsize)
+{
+ return buildExtensionBlock(c, ctx, serverHelloBuilders,
+ nelem(serverHelloBuilders), out, outsize);
+}
+
+/*
+ * RFC 5246 Section 7.4.1.4: client MUST abort with
+ * unsupported_extension(110) if the server's ServerHello carries any
+ * extension type the client did not advertise. This implementation
+ * tracks advertised types and rejects unsolicited extensions strictly.
+ */
+static int
+serverHelloParseExt(TlsConnection *c, ExtCtx *ctx, uchar *in, int inlen)
+{
+ uchar *p, *e;
+ int i, t, nn, offered;
+
+ p = in;
+ e = in + inlen;
+ while(p + 4 <= e){
+ t = get16(p);
+ nn = get16(p+2);
+ p += 4;
+ if(p + nn > e)
+ return -1;
+ offered = 0;
+ for(i = 0; i < c->nClientOffered; i++)
+ if(c->clientOffered[i] == t){
+ offered = 1;
+ break;
+ }
+ if(!offered){
+ tlsError(c, EUnsupportedExtension,
+ "server sent unsolicited extension %#x", t);
+ return -1;
+ }
+ p += nn;
+ }
+ return parseExtensionBlock(c, ctx, serverHelloParsers,
+ nelem(serverHelloParsers), in, inlen);
}
/*
|