Plan 9 from Bell Labs’s /usr/web/sources/contrib/mospak/tls-modern-client/libsec-x509-validity-dates.patch

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


libsec: X.509 notBefore/notAfter validity-window enforcement

Extends X509verifychain (added by libsec-x509-chain-hostname)
to reject certs outside their validity window.  Every cert in
the chain is checked against time(0); a cert with notAfter in
the past or notBefore in the future fails the handshake with
a short, human-readable error.

Two new static helpers in x509.c:
    asn1time_to_sec()    parse ASN.1 UTCTime "YYMMDDHHMMSSZ"
                         (RFC 5280 §4.1.2.5: YY < 50 is 20YY,
                         else 19YY) or GeneralizedTime
                         "YYYYMMDDHHMMSSZ".  Z (UTC) only,
                         which is all 5280 permits.
    cert_validity_check() decode cert, compare validity_start
                         and validity_end against now.
                         Returns "cert not yet valid",
                         "cert expired", or "cert has
                         unparseable validity dates".

X509verifychain gains a per-chain preflight loop that applies
cert_validity_check to every cert with now = time(0), placed
after the leaf hostname match and before the pairwise
signature walk.  The public signature is unchanged.

Before this patch, expired.badssl.com completed chain
verification successfully — the cert chains up to DigiCert,
the hostname matches, and the verifier did not look at the
notAfter.

BUG: basicConstraints CA:TRUE, keyUsage/extKeyUsage, and
nameConstraints are still not checked.

RFC 5280 §4.1.2.5 (Validity), §6.1.3(a)(2) (chain MUST check
current time is within every cert's validity period).

--- sys/src/libsec/port/x509.c
+++ sys/src/libsec/port/x509.c
@@ -2760,15 +2760,85 @@
 }
 
 /*
+ * asn1time_to_sec: parse an ASN.1 time string into seconds since epoch.
+ * Accepts UTCTime "YYMMDDHHMMSSZ" (RFC 5280 §4.1.2.5: YY < 50 is 20YY,
+ * else 19YY) or GeneralizedTime "YYYYMMDDHHMMSSZ".  Z (UTC) only, which
+ * is all RFC 5280 permits in X.509.  Returns -1 on parse failure.
+ */
+static long
+asn1time_to_sec(char *s)
+{
+	Tm tm;
+	int n, y;
+
+	if(s == nil)
+		return -1;
+	memset(&tm, 0, sizeof tm);
+	strcpy(tm.zone, "GMT");
+	n = strlen(s);
+	if(n == 13 && s[12] == 'Z'){
+		y = (s[0]-'0')*10 + (s[1]-'0');
+		tm.year = (y < 50 ? 100 + y : y);
+		tm.mon  = (s[2]-'0')*10 + (s[3]-'0') - 1;
+		tm.mday = (s[4]-'0')*10 + (s[5]-'0');
+		tm.hour = (s[6]-'0')*10 + (s[7]-'0');
+		tm.min  = (s[8]-'0')*10 + (s[9]-'0');
+		tm.sec  = (s[10]-'0')*10 + (s[11]-'0');
+	}else if(n == 15 && s[14] == 'Z'){
+		y = (s[0]-'0')*1000 + (s[1]-'0')*100
+		  + (s[2]-'0')*10 + (s[3]-'0');
+		tm.year = y - 1900;
+		tm.mon  = (s[4]-'0')*10 + (s[5]-'0') - 1;
+		tm.mday = (s[6]-'0')*10 + (s[7]-'0');
+		tm.hour = (s[8]-'0')*10 + (s[9]-'0');
+		tm.min  = (s[10]-'0')*10 + (s[11]-'0');
+		tm.sec  = (s[12]-'0')*10 + (s[13]-'0');
+	}else
+		return -1;
+	return tm2sec(&tm);
+}
+
+/*
+ * cert_validity_check: decode cert, compare [notBefore, notAfter]
+ * against `now`.  Returns nil on OK or a short error string.
+ */
+static char*
+cert_validity_check(uchar *pem, int pemlen, long now)
+{
+	Bytes *b;
+	CertX509 *c;
+	long nb, na;
+	char *err;
+
+	b = makebytes(pem, pemlen);
+	c = decode_cert(b);
+	freebytes(b);
+	if(c == nil)
+		return "cannot decode cert for validity check";
+	err = nil;
+	nb = asn1time_to_sec(c->validity_start);
+	na = asn1time_to_sec(c->validity_end);
+	if(nb < 0 || na < 0)
+		err = "cert has unparseable validity dates";
+	else if(now < nb)
+		err = "cert not yet valid";
+	else if(now > na)
+		err = "cert expired";
+	freecert(c);
+	return err;
+}
+
+/*
  * Walk a certificate chain (leaf at head of PEMChain list, intermediates
  * following).  Verifies each cert's signature against the next cert in
  * the chain, then verifies the top of the chain against one of the trust
- * anchors in `roots`.  Also calls X509matchhostname on the leaf when
+ * anchors in `roots`.  Checks the validity window (notBefore/notAfter)
+ * of every cert.  Also calls X509matchhostname on the leaf when
  * `hostname` is non-nil/empty.
  *
- * BUG: does not currently check validity dates, basicConstraints CA:TRUE,
- * keyUsage/extKeyUsage, or nameConstraints.  The cryptographic chain and
- * hostname binding are enforced.
+ * BUG: does not currently check basicConstraints CA:TRUE, keyUsage /
+ * extKeyUsage, or nameConstraints.  Cryptographic chain, hostname
+ * binding, and validity window are enforced.
  *
  * Returns nil on success, error string on failure.
  */
@@ -2777,12 +2847,23 @@
 {
 	char *err;
 	PEMChain *cur, *r;
+	long now;
 
 	if(chain == nil || chain->pem == nil)
 		return "empty chain";
 
 	if(hostname != nil && *hostname != 0){
 		err = X509matchhostname(chain->pem, chain->pemlen, hostname);
+		if(err != nil)
+			return err;
+	}
+
+	/* validity window on every cert in the chain (RFC 5280 §6.1.3(a)(2)) */
+	now = time(0);
+	for(cur = chain; cur != nil; cur = cur->next){
+		if(cur->pem == nil)
+			continue;
+		err = cert_validity_check(cur->pem, cur->pemlen, now);
 		if(err != nil)
 			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.