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