Plan 9 from Bell Labs’s /usr/web/sources/contrib/mospak/tls-modern-client/tls-ecdsa-and-chain-integration.patch

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


tls: ECDHE_ECDSA suites + opt-in chain verify + hget/webfs SNI

Wires the ECC primitives (libsec-ecc-ecdsa-primitives) and
X.509 verification helpers (libsec-x509-chain-hostname) into
the TLS client, adds ECDHE_ECDSA cipher suites, fixes a latent
in-kernel devtls check, and makes hget and webfs populate
conn.serverName so SNI is actually emitted.

Chain verification is opt-in.  TLSconn gains
    PEMChain *rootCAchain;
When a caller sets it before tlsClient(), X509verifychain runs
on the server's cert list and fails the handshake on
verification error.  When left nil, pre-patch thumbprint-only
behaviour is preserved for existing callers.

verifyDHparams dispatches on the sigalg low byte:
    0x01   rsapkcs1verify
    0x03   X509toECpub + X509ecdsaverifydigest

Two new cipher suite IDs (outside the weakCipher[] range):
    TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256       0xC02B
    TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 0xCCA9

devtls's "missing algorithm implementations" check is relaxed
to accept either the legacy (mac+enc+dec) or AEAD
(aead_enc+aead_dec) function-pointer set.  The pre-patch
check demanded both enc and dec on every Secret, which AEAD
init functions deliberately leave nil.

hget and webfs set conn.serverName before tlsClient so modern
servers (Cloudflare, Let's Encrypt edges, etc.) pick the
right cert rather than returning a default or refusing.

RFC 5246 §7.4.3 (SKE signature over params);
8422 (TLS_ECDHE_ECDSA_* suites, named_curves,
ec_point_formats); 6066 §3 (SNI);
5280 + 6125 (opt-in chain + hostname check).

--- sys/include/libsec.h
+++ sys/include/libsec.h
@@ -510,6 +510,7 @@
 	int	sessionKeylen;
 	char	*sessionConst;
 	char	*serverName;	/* SNI: set by client before tlsClient; set by tlsServer to observed SNI */
+	PEMChain *rootCAchain;	/* trust anchors for X509verifychain; nil = no chain validation */
 } TLSconn;
 
 /* tlshand.c */
--- sys/src/9/port/devtls.c
+++ sys/src/9/port/devtls.c
@@ -1713,8 +1713,11 @@
 		(*ea->initkey)(ea, tos, &x[2 * ha->maclen], &x[2 * ha->maclen + 2 * ea->keylen]);
 		(*ea->initkey)(ea, toc, &x[2 * ha->maclen + ea->keylen], &x[2 * ha->maclen + 2 * ea->keylen + ea->ivlen]);
 
-		if(!tos->mac || !tos->enc || !tos->dec
-		|| !toc->mac || !toc->enc || !toc->dec)
+		/* Either legacy (mac+enc+dec) or AEAD (aead_enc+aead_dec)
+		 * pathways must be fully wired on both directions. */
+		if(!tos->mac || !toc->mac
+		|| ((!tos->enc || !tos->dec || !toc->enc || !toc->dec)
+		   && (!tos->aead_enc || !tos->aead_dec || !toc->aead_enc || !toc->aead_dec)))
 			error("missing algorithm implementations");
 		if(strtol(cb->f[3], nil, 0) == 0){
 			tr->in.new = tos;
--- sys/src/9k/port/devtls.c
+++ sys/src/9k/port/devtls.c
@@ -1713,8 +1713,11 @@
 		(*ea->initkey)(ea, tos, &x[2 * ha->maclen], &x[2 * ha->maclen + 2 * ea->keylen]);
 		(*ea->initkey)(ea, toc, &x[2 * ha->maclen + ea->keylen], &x[2 * ha->maclen + 2 * ea->keylen + ea->ivlen]);
 
-		if(!tos->mac || !tos->enc || !tos->dec
-		|| !toc->mac || !toc->enc || !toc->dec)
+		/* Either legacy (mac+enc+dec) or AEAD (aead_enc+aead_dec)
+		 * pathways must be fully wired on both directions. */
+		if(!tos->mac || !toc->mac
+		|| ((!tos->enc || !tos->dec || !toc->enc || !toc->dec)
+		   && (!tos->aead_enc || !tos->aead_dec || !toc->aead_enc || !toc->aead_dec)))
 			error("missing algorithm implementations");
 		if(strtol(cb->f[3], nil, 0) == 0){
 			tr->in.new = tos;
--- sys/src/cmd/hget.c
+++ sys/src/cmd/hget.c
@@ -353,6 +353,7 @@
 			TLSconn conn;
 
 			memset(&conn, 0, sizeof conn);
+			conn.serverName = u->host;	/* SNI: modern servers require it */
 			tfd = tlsClient(fd, &conn);
 			if(tfd < 0){
 				fprint(2, "tlsClient: %r\n");
--- sys/src/cmd/webfs/fns.h
+++ sys/src/cmd/webfs/fns.h
@@ -34,7 +34,7 @@
 void		httpclose(Client*);
 
 /* io.c */
-int		iotlsdial(Ioproc*, char*, char*, char*, int*, int);
+int		iotlsdial(Ioproc*, char*, char*, char*, int*, int, char*);
 int		ioprint(Ioproc*, int, char*, ...);
 #pragma varargck argpos ioprint 3
 
--- sys/src/cmd/webfs/http.c
+++ sys/src/cmd/webfs/http.c
@@ -286,7 +286,7 @@
 		fprint(2, "dial %s\n", hs->netaddr);
 		fprint(2, "dial port: %s\n", url->port);
 	}
-	fd = iotlsdial(io, hs->netaddr, 0, 0, 0, url->ischeme==UShttps);
+	fd = iotlsdial(io, hs->netaddr, 0, 0, 0, url->ischeme==UShttps, url->host);
 	if(fd < 0){
 	Error:
 		if(httpdebug)
--- sys/src/cmd/webfs/io.c
+++ sys/src/cmd/webfs/io.c
@@ -45,7 +45,7 @@
 static long
 _iotlsdial(va_list *arg)
 {
-	char *addr, *local, *dir;
+	char *addr, *local, *dir, *host;
 	int *cfdp, fd, tfd, usetls;
 	TLSconn conn;
 
@@ -54,6 +54,7 @@
 	dir = va_arg(*arg, char*);
 	cfdp = va_arg(*arg, int*);
 	usetls = va_arg(*arg, int);
+	host = va_arg(*arg, char*);
 
 	fd = dial(addr, local, dir, cfdp);
 	if(fd < 0)
@@ -62,8 +63,7 @@
 		return fd;
 
 	memset(&conn, 0, sizeof conn);
-	/* does no good, so far anyway */
-	// conn.chain = readcertchain("/sys/lib/ssl/vsignss.pem");
+	conn.serverName = host;	/* SNI: modern servers pick a cert based on it */
 
 	tfd = tlsClient(fd, &conn);
 	close(fd);
@@ -78,7 +78,7 @@
 }
 
 int
-iotlsdial(Ioproc *io, char *addr, char *local, char *dir, int *cfdp, int usetls)
+iotlsdial(Ioproc *io, char *addr, char *local, char *dir, int *cfdp, int usetls, char *host)
 {
-	return iocall(io, _iotlsdial, addr, local, dir, cfdp, usetls);
+	return iocall(io, _iotlsdial, addr, local, dir, cfdp, usetls, host);
 }
--- sys/src/libsec/port/tlshand.c
+++ sys/src/libsec/port/tlshand.c
@@ -259,7 +259,9 @@
  * okCipher() treats c >= CipherMax as not-weak, which is correct here.
  */
 enum {
+	TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256		= 0xC02B,
 	TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256		= 0xC02F,
+	TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256	= 0xCCA9,
 	TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256	= 0xCCA8,
 };
 
@@ -269,12 +271,18 @@
 	CompressionMax
 };
 
-// extensions (RFC 6066, 4492, 5246)
+// extensions (RFC 6066, 4492, 5246, 7301, 7627, 5746, 8446)
 enum {
 	ExtSni			= 0x0000,
+	ExtStatusRequest	= 0x0005,
 	ExtSupportedGroups	= 0x000a,
 	ExtEcPointFormats	= 0x000b,
 	ExtSigalgs		= 0x000d,
+	ExtAlpn			= 0x0010,
+	ExtExtendedMasterSecret	= 0x0017,
+	ExtSessionTicket	= 0x0023,
+	ExtSupportedVersions	= 0x002b,
+	ExtRenegotiationInfo	= 0xff01,
 };
 
 // supported ECDHE named groups (we offer X25519 only)
@@ -287,16 +295,22 @@
 	RSA_PKCS1_SHA1   = 0x0201,
 	RSA_PKCS1_SHA256 = 0x0401,
 	RSA_PKCS1_SHA384 = 0x0501,
-	RSA_PKCS1_SHA512 = 0x0601
+	RSA_PKCS1_SHA512 = 0x0601,
+	ECDSA_SECP256R1_SHA256 = 0x0403,
+	ECDSA_SECP384R1_SHA384 = 0x0503,
 };
 
 static Algs cipherAlgs[] = {
-	/* ECDHE_RSA + AEAD — preferred; safe against MITM given SKE-sig verification */
+	/* ECDHE + AEAD — preferred; safe against MITM given SKE-sig verification.
+	 * ECDSA suites listed first so servers with ECDSA-only certs (Cloudflare,
+	 * most modern sites) see a compatible offer. */
+	{"aes_128_gcm_aead", "clear", 2*(16+4), TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
+	{"ccpoly96_aead", "clear", 2*(32+12), TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256},
 	{"aes_128_gcm_aead", "clear", 2*(16+4), TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
 	{"ccpoly96_aead", "clear", 2*(32+12), TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256},
-	/* legacy RSA-KE suites (no forward secrecy) */
-	{"rc4_128", "md5", 2*(16+MD5dlen), TLS_RSA_WITH_RC4_128_MD5},
-	{"rc4_128", "sha1", 2*(16+SHA1dlen), TLS_RSA_WITH_RC4_128_SHA},
+	/* legacy RSA-KE suites (no forward secrecy).  RC4 suites (RFC 7465)
+	 * are intentionally omitted — some modern servers reject clients
+	 * that offer RC4 at all, regardless of other suites available. */
 	{"3des_ede_cbc", "sha1", 2*(4*8+SHA1dlen), TLS_RSA_WITH_3DES_EDE_CBC_SHA},
 	{"aes_128_cbc", "sha1", 2*(16+16+SHA1dlen), TLS_RSA_WITH_AES_128_CBC_SHA},
 	{"aes_256_cbc", "sha1", 2*(32+16+SHA1dlen), TLS_RSA_WITH_AES_256_CBC_SHA}
@@ -307,13 +321,16 @@
 };
 
 /*
- * Advertised signature_algorithms.  Keep in sync with rsapkcs1verify
- * (which only handles SHA-256 today); advertising a hash we don't
- * verify creates a failure mode where the server picks it and we
- * then reject the SKE signature.  SHA-1 for TLS 1.2 signatures is
- * deprecated (RFC 9155) and modern servers don't pick it anyway.
+ * Advertised signature_algorithms.  RSA_PKCS1_SHA1 was advertised at
+ * one point but our rsapkcs1verify only accepts SHA-256, so offering
+ * SHA-1 would create a failure mode where a server picks it and we
+ * then reject.  SHA-1 is deprecated for TLS 1.2 signatures in any case
+ * (RFC 9155).  ECDSA hashes we also verify (SHA-256 and SHA-384 in
+ * verifyDHparams).
  */
 static int sigAlgs[] = {
+	ECDSA_SECP256R1_SHA256,
+	ECDSA_SECP384R1_SHA384,
 	RSA_PKCS1_SHA256,
 };
 
@@ -328,7 +345,7 @@
 };
 
 static TlsConnection *tlsServer2(int ctl, int hand, uchar *cert, int ncert, int (*trace)(char*fmt, ...), PEMChain *chain);
-static TlsConnection *tlsClient2(int ctl, int hand, uchar *csid, int ncsid, char *serverName, int (*trace)(char*fmt, ...));
+static TlsConnection *tlsClient2(int ctl, int hand, uchar *csid, int ncsid, char *serverName, PEMChain *rootCA, int (*trace)(char*fmt, ...));
 
 static void	msgClear(Msg *m);
 static char* msgPrint(char *buf, int n, Msg *m);
@@ -485,7 +502,7 @@
 		return -1;
 	}
 	fprint(ctl, "fd %d 0x%x", fd, ProtocolVersion);
-	tls = tlsClient2(ctl, hand, conn->sessionID, conn->sessionIDlen, conn->serverName, conn->trace);
+	tls = tlsClient2(ctl, hand, conn->sessionID, conn->sessionIDlen, conn->serverName, conn->rootCAchain, conn->trace);
 	close(fd);
 	close(hand);
 	close(ctl);
@@ -689,7 +706,7 @@
 }
 
 static TlsConnection *
-tlsClient2(int ctl, int hand, uchar *csid, int ncsid, char *serverName, int (*trace)(char*fmt, ...))
+tlsClient2(int ctl, int hand, uchar *csid, int ncsid, char *serverName, PEMChain *rootCA, int (*trace)(char*fmt, ...))
 {
 	TlsConnection *c;
 	Msg m;
@@ -765,6 +782,36 @@
 		goto Err;
 	}
 	c->cert = makebytes(m.u.certificate.certs[0]->data, m.u.certificate.certs[0]->len);
+
+	/*
+	 * Full X.509 chain and hostname verification (RFC 5280 + RFC 6125).
+	 * Skipped when rootCA is nil to preserve the thumbprint-only trust
+	 * model for callers that haven't yet provided trust anchors.
+	 */
+	if(rootCA != nil){
+		PEMChain *ch, *tc;
+		char *verr;
+		int i;
+
+		ch = nil;
+		for(i = m.u.certificate.ncert - 1; i >= 0; i--){
+			tc = emalloc(sizeof(PEMChain));
+			tc->next = ch;
+			tc->pem = m.u.certificate.certs[i]->data;
+			tc->pemlen = m.u.certificate.certs[i]->len;
+			ch = tc;
+		}
+		verr = X509verifychain(ch, rootCA, serverName);
+		while(ch != nil){
+			tc = ch->next;
+			free(ch);	/* frees only the list nodes; pem pointers belong to m */
+			ch = tc;
+		}
+		if(verr != nil){
+			tlsError(c, EBadCertificate, "cert verify: %s", verr);
+			goto Err;
+		}
+	}
 	msgClear(&m);
 
 	/* server key exchange (mandatory for ECDHE, forbidden for RSA-KE) */
@@ -2129,6 +2176,8 @@
 	switch(tlsid){
 	case TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:
 	case TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:
+	case TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:
+	case TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:
 		return 1;
 	}
 	return 0;
@@ -2224,8 +2273,13 @@
 {
 	uchar *blob;
 	int bloblen;
-	RSApub *pk;
+	int sigalg;
 	char *err;
+	RSApub *rsa;
+	ECpub *ec;
+	ECdomain dom;
+	uchar digest[SHA2_512dlen];
+	int digestlen;
 
 	if(cert == nil || cert->len == 0)
 		return "no server certificate";
@@ -2233,9 +2287,7 @@
 	   m->u.serverKeyExchange.params == nil)
 		return "no SKE signature or params";
 
-	pk = X509toRSApub(cert->data, cert->len, nil, 0);
-	if(pk == nil)
-		return "bad certificate";
+	sigalg = m->u.serverKeyExchange.sigalg;
 
 	bloblen = 2*RandomSize + m->u.serverKeyExchange.params->len;
 	blob = emalloc(bloblen);
@@ -2244,13 +2296,56 @@
 	memmove(blob + 2*RandomSize, m->u.serverKeyExchange.params->data,
 		m->u.serverKeyExchange.params->len);
 
-	err = rsapkcs1verify(pk, m->u.serverKeyExchange.sigalg,
-		blob, bloblen,
-		m->u.serverKeyExchange.signature->data,
-		m->u.serverKeyExchange.signature->len);
+	err = "unsupported SKE signature algorithm";
+	switch(sigalg & 0xff){
+	case 0x01:	/* RSA PKCS#1 v1.5 */
+		rsa = X509toRSApub(cert->data, cert->len, nil, 0);
+		if(rsa == nil){
+			err = "bad RSA certificate";
+			break;
+		}
+		err = rsapkcs1verify(rsa, sigalg, blob, bloblen,
+			m->u.serverKeyExchange.signature->data,
+			m->u.serverKeyExchange.signature->len);
+		rsapubfree(rsa);
+		break;
 
+	case 0x03:	/* ECDSA (hash in high byte, algo in low byte) */
+		digestlen = 0;
+		switch((sigalg >> 8) & 0xff){
+		case 0x04:
+			sha2_256(blob, bloblen, digest, nil);
+			digestlen = SHA2_256dlen;
+			break;
+		case 0x05:
+			sha2_384(blob, bloblen, digest, nil);
+			digestlen = SHA2_384dlen;
+			break;
+		case 0x06:
+			sha2_512(blob, bloblen, digest, nil);
+			digestlen = SHA2_512dlen;
+			break;
+		}
+		if(digestlen == 0){
+			werrstr("sigalg=%#x", sigalg);
+			err = "unsupported ECDSA SKE hash algorithm";
+			break;
+		}
+		ec = X509toECpub(cert->data, cert->len, nil, 0, &dom);
+		if(ec == nil){
+			err = "bad ECDSA certificate";
+			break;
+		}
+		err = X509ecdsaverifydigest(
+			m->u.serverKeyExchange.signature->data,
+			m->u.serverKeyExchange.signature->len,
+			digest, digestlen, &dom, ec);
+		ecpubfree(ec);
+		ecdomfree(&dom);
+		break;
+	}
+
 	free(blob);
-	rsapubfree(pk);
 	return err;
 }
 

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.