Plan 9 from Bell Labs’s /usr/web/sources/contrib/mospak/tls-1.2/tls-extension-framework.diff

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


--- 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);
 }
 
 /*

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.