Plan 9 from Bell Labs’s /usr/web/sources/contrib/mospak/tls-1.2/libsec-x509-rfc5280-constraints.diff

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


--- sys/src/libsec/port/x509.c
+++ sys/src/libsec/port/x509.c
@@ -1570,6 +1570,13 @@ struct CertExtension {
 	Bytes*		value;		/* extnValue OCTET STRING contents */
 };
 
+typedef struct GeneralSubtree GeneralSubtree;
+struct GeneralSubtree {
+	GeneralSubtree*	next;
+	int		type;		/* GeneralName form: 1=rfc822Name, 2=dNSName, 7=iPAddress */
+	Bytes*		value;		/* raw GeneralName value bytes */
+};
+
 typedef struct CertX509 {
 	int	serial;
 	char*	issuer;
@@ -1584,6 +1591,30 @@ typedef struct CertX509 {
 	Bytes*	ext;		/* raw extensions SEQUENCE (opaque); nil if cert has none */
 	CertExtension* extensions;	/* parsed list, nil if no extensions or unparseable */
 	int	unrecognizedCritical;	/* RFC 5280 Section 4.2 flag, checked by validator */
+
+	/* RFC 5280 Section 3.2: subject DN == issuer DN.
+	 * Distinct from self-signed (which also requires verifiability
+	 * by own key); CA key-rollover certs are self-issued but not
+	 * self-signed. */
+	int	isSelfIssued;
+
+	/* RFC 5280 Section 4.2.1.9 basicConstraints */
+	int	hasBasicConstraints;
+	int	isCA;
+	int	pathLen;	/* -1 if not specified */
+
+	/* RFC 5280 Section 4.2.1.3 keyUsage */
+	int	hasKeyUsage;
+	ulong	keyUsageBits;	/* see KU_* */
+
+	/* RFC 5280 Section 4.2.1.12 extKeyUsage */
+	int	hasExtKeyUsage;
+	ulong	extKeyUsageBits;	/* see EKU_* */
+
+	/* RFC 5280 Section 4.2.1.10 nameConstraints */
+	int	hasNameConstraints;
+	GeneralSubtree*	permittedSubtrees;
+	GeneralSubtree*	excludedSubtrees;
 } CertX509;
 
 /* Algorithm object-ids */
@@ -1668,6 +1699,46 @@ static DigestFun digestalg[NUMALGS+1] = {
 	nil
 };
 
+/* RFC 5280 Section 4.2 X.509v3 extension OIDs */
+static Ints9 oid_subjectAltName		= {4, 2, 5, 29, 17};
+static Ints9 oid_basicConstraints	= {4, 2, 5, 29, 19};
+static Ints9 oid_keyUsage		= {4, 2, 5, 29, 15};
+static Ints9 oid_extKeyUsage		= {4, 2, 5, 29, 37};
+static Ints9 oid_nameConstraints	= {4, 2, 5, 29, 30};
+
+/* RFC 5280 Section 4.2.1.12 extKeyUsage purposes */
+static Ints9 oid_anyExtendedKeyUsage	= {5, 2, 5, 29, 37, 0};
+static Ints9 oid_serverAuth		= {8, 1, 3, 6, 1, 5, 5, 7, 3, 1};
+static Ints9 oid_clientAuth		= {8, 1, 3, 6, 1, 5, 5, 7, 3, 2};
+static Ints9 oid_codeSigning		= {8, 1, 3, 6, 1, 5, 5, 7, 3, 3};
+static Ints9 oid_emailProtection	= {8, 1, 3, 6, 1, 5, 5, 7, 3, 4};
+static Ints9 oid_timeStamping		= {8, 1, 3, 6, 1, 5, 5, 7, 3, 8};
+static Ints9 oid_OCSPSigning		= {8, 1, 3, 6, 1, 5, 5, 7, 3, 9};
+
+/* RFC 5280 Section 4.2.1.3 keyUsage bits */
+enum {
+	KU_DigitalSignature	= 1<<0,
+	KU_NonRepudiation	= 1<<1,
+	KU_KeyEncipherment	= 1<<2,
+	KU_DataEncipherment	= 1<<3,
+	KU_KeyAgreement		= 1<<4,
+	KU_KeyCertSign		= 1<<5,
+	KU_CRLSign		= 1<<6,
+	KU_EncipherOnly		= 1<<7,
+	KU_DecipherOnly		= 1<<8,
+};
+
+/* RFC 5280 Section 4.2.1.12 extKeyUsage purposes */
+enum {
+	EKU_AnyKeyUsage		= 1<<0,
+	EKU_ServerAuth		= 1<<1,
+	EKU_ClientAuth		= 1<<2,
+	EKU_CodeSigning		= 1<<3,
+	EKU_EmailProtection	= 1<<4,
+	EKU_TimeStamping	= 1<<5,
+	EKU_OCSPSigning		= 1<<6,
+};
+
 /* Named curve OIDs (parameters of an ecPublicKey SubjectPublicKeyInfo) */
 static Ints9 oid_secp256r1 = {7, 1, 2, 840, 10045, 3, 1, 7};
 static Ints9 oid_secp384r1 = {5, 1, 3, 132, 0, 34};
@@ -1689,7 +1760,12 @@ static void	freecertextensions(CertExtension* e);
 };
 
 static void	freecertextensions(CertExtension* e);
+static void	freegeneralsubtrees(GeneralSubtree* s);
 static void	parse_extensions(CertX509* c);
+static int	parse_basicConstraints(Bytes* val, CertX509* c);
+static int	parse_keyUsage(Bytes* val, CertX509* c);
+static int	parse_extKeyUsage(Bytes* val, CertX509* c);
+static int	parse_nameConstraints(Bytes* val, CertX509* c);
 
 static void
 freecert(CertX509* c)
@@ -1707,6 +1783,8 @@ freecert(CertX509* c)
 	freebytes(c->signature);
 	freebytes(c->ext);
 	freecertextensions(c->extensions);
+	freegeneralsubtrees(c->permittedSubtrees);
+	freegeneralsubtrees(c->excludedSubtrees);
 	free(c);
 }
 
@@ -1724,6 +1802,19 @@ freecertextensions(CertExtension *e)
 	}
 }
 
+static void
+freegeneralsubtrees(GeneralSubtree *s)
+{
+	GeneralSubtree *n;
+
+	while(s != nil){
+		n = s->next;
+		freebytes(s->value);
+		free(s);
+		s = n;
+	}
+}
+
 /*
  * Parse one TBSCertificate extension element per RFC 5280 Section 4.2:
  *   Extension ::= SEQUENCE { extnID OBJECT IDENTIFIER,
@@ -1767,10 +1858,267 @@ parse_one_extension(Elem *e, int *critical_out)
 }
 
 /*
+ * RFC 5280 Section 4.2.1.9 BasicConstraints:
+ *   BasicConstraints ::= SEQUENCE {
+ *       cA                      BOOLEAN DEFAULT FALSE,
+ *       pathLenConstraint       INTEGER (0..MAX) OPTIONAL }
+ */
+static int
+parse_basicConstraints(Bytes *val, CertX509 *c)
+{
+	Elem ebc;
+	Elist *l;
+	int v, ok;
+
+	if(val == nil)
+		return 0;
+	if(decode(val->data, val->len, &ebc) != ASN_OK)
+		return 0;
+	ok = 0;
+	if(!is_seq(&ebc, &l))
+		goto out;
+	c->isCA = 0;
+	c->pathLen = -1;
+	if(l != nil && l->hd.tag.class == Universal && l->hd.tag.num == BOOLEAN
+	&& l->hd.val.tag == VBool){
+		c->isCA = l->hd.val.u.boolval ? 1 : 0;
+		l = l->tl;
+	}
+	if(l != nil){
+		if(!is_int(&l->hd, &v) || v < 0)
+			goto out;
+		c->pathLen = v;
+		l = l->tl;
+	}
+	if(l != nil)
+		goto out;
+	c->hasBasicConstraints = 1;
+	ok = 1;
+out:
+	freevalfields(&ebc.val);
+	return ok;
+}
+
+/*
+ * RFC 5280 Section 4.2.1.3 KeyUsage:
+ *   KeyUsage ::= BIT STRING { ... }
+ * The leftmost (high) bit is digitalSignature; map MSB-first into KU_*.
+ */
+static int
+parse_keyUsage(Bytes *val, CertX509 *c)
+{
+	Elem eku;
+	Bits *b;
+	int i, n, ok;
+	ulong bits;
+
+	if(val == nil)
+		return 0;
+	if(decode(val->data, val->len, &eku) != ASN_OK)
+		return 0;
+	ok = 0;
+	if(!is_bitstring(&eku, &b))
+		goto out;
+	bits = 0;
+	n = b->len * 8 - b->unusedbits;	/* total significant bits */
+	if(n < 0)
+		goto out;
+	if(n > 16)
+		n = 16;
+	for(i = 0; i < n; i++){
+		if(b->data[i/8] & (0x80 >> (i&7)))
+			bits |= (1UL << i);
+	}
+	c->keyUsageBits = bits;
+	c->hasKeyUsage = 1;
+	ok = 1;
+out:
+	freevalfields(&eku.val);
+	return ok;
+}
+
+/*
+ * RFC 5280 Section 4.2.1.12 ExtKeyUsage:
+ *   ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId
+ *   KeyPurposeId ::= OBJECT IDENTIFIER
+ * Unknown OIDs are skipped silently per RFC 5280: extKeyUsage is
+ * informational; receivers process only the purposes they recognise.
+ */
+static int
+parse_extKeyUsage(Bytes *val, CertX509 *c)
+{
+	Elem eeku;
+	Elist *l;
+	Ints *oid;
+	ulong bits;
+	int ok;
+
+	if(val == nil)
+		return 0;
+	if(decode(val->data, val->len, &eeku) != ASN_OK)
+		return 0;
+	ok = 0;
+	if(!is_seq(&eeku, &l))
+		goto out;
+	bits = 0;
+	for(; l != nil; l = l->tl){
+		if(!is_oid(&l->hd, &oid))
+			continue;
+		if(ints_eq(oid, (Ints*)&oid_anyExtendedKeyUsage))
+			bits |= EKU_AnyKeyUsage;
+		else if(ints_eq(oid, (Ints*)&oid_serverAuth))
+			bits |= EKU_ServerAuth;
+		else if(ints_eq(oid, (Ints*)&oid_clientAuth))
+			bits |= EKU_ClientAuth;
+		else if(ints_eq(oid, (Ints*)&oid_codeSigning))
+			bits |= EKU_CodeSigning;
+		else if(ints_eq(oid, (Ints*)&oid_emailProtection))
+			bits |= EKU_EmailProtection;
+		else if(ints_eq(oid, (Ints*)&oid_timeStamping))
+			bits |= EKU_TimeStamping;
+		else if(ints_eq(oid, (Ints*)&oid_OCSPSigning))
+			bits |= EKU_OCSPSigning;
+		/* unknown KeyPurposeId: skip silently */
+	}
+	c->extKeyUsageBits = bits;
+	c->hasExtKeyUsage = 1;
+	ok = 1;
+out:
+	freevalfields(&eeku.val);
+	return ok;
+}
+
+/*
+ * Parse the body bytes of a [0] permittedSubtrees or [1] excludedSubtrees
+ * IMPLICIT GeneralSubtrees field.  RFC 5280 Section A.1 declares the X.509
+ * module IMPLICIT TAGS, so the body is the bare concatenation of
+ * GeneralSubtree SEQUENCE elements (no outer SEQUENCE wrapper).  Append
+ * parsed nodes to *listp.  Returns 1 on success, 0 on parse failure.
+ */
+static int
+parse_subtrees(Bytes *raw, GeneralSubtree **listp)
+{
+	Elist *l, *gl, *p;
+	GeneralSubtree *node, **tail;
+	Bytes *src;
+	int form, ok, mn;
+	Elem ei;
+
+	if(raw == nil || raw->len == 0)
+		return 0;
+	l = nil;
+	if(decode_seq(raw->data, raw->len, &l) != ASN_OK)
+		return 0;
+	ok = 0;
+	tail = listp;
+	while(*tail != nil)
+		tail = &(*tail)->next;
+	for(p = l; p != nil; p = p->tl){
+		/* each GeneralSubtree is a SEQUENCE { GeneralName, ... } */
+		if(p->hd.val.tag != VSeq)
+			goto out;
+		gl = p->hd.val.u.seqval;
+		if(gl == nil)
+			goto out;
+		if(gl->hd.tag.class != Context)
+			goto out;
+		form = gl->hd.tag.num;
+		if(form != 1 && form != 2 && form != 7)
+			goto out;	/* unsupported GeneralName form */
+		if(gl->hd.val.tag != VOctets)
+			goto out;
+		src = gl->hd.val.u.octetsval;
+		/* Empty-base GeneralName has no constraint semantics:
+		 * as permitted it matches the entire namespace; as
+		 * excluded it blocks everything. */
+		if(src == nil || src->len == 0)
+			goto out;
+		/* RFC 5280 Section 4.2.1.10: minimum MUST be zero (DEFAULT 0,
+		 * encoded as absent), maximum MUST be absent. */
+		for(gl = gl->tl; gl != nil; gl = gl->tl){
+			if(gl->hd.tag.class != Context)
+				goto out;
+			if(gl->hd.tag.num != 0)
+				goto out;	/* maximum [1] forbidden */
+			if(gl->hd.val.tag != VOctets || gl->hd.val.u.octetsval == nil)
+				goto out;
+			if(decode(gl->hd.val.u.octetsval->data,
+				gl->hd.val.u.octetsval->len, &ei) != ASN_OK)
+				goto out;
+			if(!is_int(&ei, &mn) || mn != 0){
+				freevalfields(&ei.val);
+				goto out;
+			}
+			freevalfields(&ei.val);
+		}
+		node = (GeneralSubtree*)emalloc(sizeof(GeneralSubtree));
+		node->next = nil;
+		node->type = form;
+		node->value = makebytes(src->data, src->len);
+		*tail = node;
+		tail = &node->next;
+	}
+	ok = 1;
+out:
+	freeelist(l);
+	return ok;
+}
+
+/*
+ * RFC 5280 Section 4.2.1.10 NameConstraints:
+ *   NameConstraints ::= SEQUENCE {
+ *       permittedSubtrees       [0] GeneralSubtrees OPTIONAL,
+ *       excludedSubtrees        [1] GeneralSubtrees OPTIONAL }
+ */
+static int
+parse_nameConstraints(Bytes *val, CertX509 *c)
+{
+	Elem enc;
+	Elist *l;
+	Bytes *raw;
+	int tagn, got, ok;
+
+	if(val == nil)
+		return 0;
+	if(decode(val->data, val->len, &enc) != ASN_OK)
+		return 0;
+	ok = 0;
+	got = 0;
+	if(!is_seq(&enc, &l))
+		goto out;
+	for(; l != nil; l = l->tl){
+		if(l->hd.tag.class != Context)
+			goto out;
+		if(l->hd.val.tag != VOctets)
+			goto out;
+		tagn = l->hd.tag.num;
+		raw = l->hd.val.u.octetsval;
+		if(tagn == 0){
+			if(!parse_subtrees(raw, &c->permittedSubtrees))
+				goto out;
+			got = 1;
+		}else if(tagn == 1){
+			if(!parse_subtrees(raw, &c->excludedSubtrees))
+				goto out;
+			got = 1;
+		}else{
+			goto out;
+		}
+	}
+	if(got)
+		c->hasNameConstraints = 1;
+	ok = got;
+out:
+	freevalfields(&enc.val);
+	return ok;
+}
+
+/*
  * Walk c->ext (raw SEQUENCE OF Extension) and build the c->extensions
- * list.  Sets c->unrecognizedCritical if any critical extension is
- * encountered (refined by per-OID recognition in libsec-x509-rfc5280-
- * constraints).  Silent on parse failure: c->extensions stays nil.
+ * list.  Dispatches recognized OIDs to typed parsers; sets
+ * c->unrecognizedCritical when a critical extension is seen but its
+ * OID is not recognized (or fails to parse).  Silent on outer parse
+ * failure: c->extensions stays nil.
  */
 static void
 parse_extensions(CertX509 *c)
@@ -1778,7 +2126,7 @@ parse_extensions(CertX509 *c)
 	Elem eext;
 	Elist *el;
 	CertExtension *ext, **tail;
-	int critical;
+	int critical, unhandled;
 
 	if(c->ext == nil)
 		return;
@@ -1801,7 +2149,20 @@ parse_extensions(CertX509 *c)
 			continue;
 		*tail = ext;
 		tail = &ext->next;
-		if(ext->critical)
+
+		unhandled = 1;
+		if(ints_eq(ext->oid, (Ints*)&oid_basicConstraints))
+			unhandled = !parse_basicConstraints(ext->value, c);
+		else if(ints_eq(ext->oid, (Ints*)&oid_keyUsage))
+			unhandled = !parse_keyUsage(ext->value, c);
+		else if(ints_eq(ext->oid, (Ints*)&oid_extKeyUsage))
+			unhandled = !parse_extKeyUsage(ext->value, c);
+		else if(ints_eq(ext->oid, (Ints*)&oid_nameConstraints))
+			unhandled = !parse_nameConstraints(ext->value, c);
+		else if(ints_eq(ext->oid, (Ints*)&oid_subjectAltName))
+			unhandled = 0;	/* enforced separately by X509matchhostname */
+
+		if(ext->critical && unhandled)
 			c->unrecognizedCritical = 1;
 	}
 	freevalfields(&eext.val);
@@ -2044,6 +2405,9 @@ decode_cert(Bytes* a)
  	if(!is_bitstring(esig, &bits))
 		goto errret;
  	c->signature = makebytes(bits->data, bits->len);
+	if(c->subject != nil && c->issuer != nil
+	&& strcmp(c->subject, c->issuer) == 0)
+		c->isSelfIssued = 1;
 	ok = 1;
 
 errret:
@@ -2598,10 +2962,6 @@ X509verify(uchar *cert, int ncert, RSApub *pk)
 	return e;
 }
 
-/* OID for SubjectAltName X.509v3 extension
- * (id-ce-subjectAltName, 2.5.29.17) */
-static Ints9 oid_subjectAltName = {4, 2, 5, 29, 17};
-
 /* digest length table, parallel to digestalg[] */
 static int digestlen[NUMALGS+1] = {
 	MD5dlen, MD5dlen, MD5dlen, MD5dlen, SHA1dlen, SHA1dlen,
@@ -2987,6 +3347,451 @@ cert_validity_check(CertX509 *c, long now)
 }
 
 /*
+ * RFC 6125 case-insensitive DNS-suffix match.  base is the constraint
+ * dNSName, name is a candidate from a leaf SAN.  An empty base ("")
+ * matches any name.  Otherwise name must equal base or end with "." +
+ * base, and the boundary must align on a label.
+ */
+static int
+dns_in_subtree(char *base, int blen, char *name, int nlen)
+{
+	if(blen == 0)
+		return 1;
+	if(nlen == blen && cistrncmp(base, name, blen) == 0)
+		return 1;
+	if(nlen > blen + 1 && name[nlen-blen-1] == '.'
+	&& cistrncmp(base, name + nlen - blen, blen) == 0)
+		return 1;
+	return 0;
+}
+
+/*
+ * Iterate dNSName SAN entries on `c` and call `check(base, len, ctx)`
+ * for each.  check returns nil to accept, error string to reject.
+ * Returns first rejection or nil.  No SAN: returns nil (no match to
+ * test).
+ */
+static char*
+walk_dns_san(CertX509 *c, char *(*check)(char*, int, void*), void *ctx)
+{
+	Elem eext, ealt;
+	Elist *el, *l;
+	Ints *oid;
+	Bytes *altoct, *name;
+	char *err;
+
+	if(c->ext == nil)
+		return nil;
+	if(decode(c->ext->data, c->ext->len, &eext) != ASN_OK)
+		return nil;
+	err = nil;
+	if(!is_seq(&eext, &el)){
+		freevalfields(&eext.val);
+		return nil;
+	}
+	for(; el != nil && err == nil; el = el->tl){
+		if(!is_seq(&el->hd, &l) || elistlen(l) < 2)
+			continue;
+		if(!is_oid(&l->hd, &oid) || !ints_eq(oid, (Ints*)&oid_subjectAltName))
+			continue;
+		l = l->tl;
+		if(l->hd.val.tag == VBool)
+			l = l->tl;
+		if(l == nil || !is_octetstring(&l->hd, &altoct))
+			continue;
+		if(decode(altoct->data, altoct->len, &ealt) != ASN_OK)
+			continue;
+		if(!is_seq(&ealt, &l)){
+			freevalfields(&ealt.val);
+			continue;
+		}
+		for(; l != nil && err == nil; l = l->tl){
+			if(l->hd.tag.class != Context || l->hd.tag.num != 2)
+				continue;
+			if(l->hd.val.tag != VOctets)
+				continue;
+			name = l->hd.val.u.octetsval;
+			err = check((char*)name->data, name->len, ctx);
+		}
+		freevalfields(&ealt.val);
+	}
+	freevalfields(&eext.val);
+	return err;
+}
+
+/*
+ * RFC 5280 Section 4.2.1.10 iPAddress subtree match.  The subtree
+ * value carries IP||MASK octets: 8 bytes for IPv4, 32 for IPv6.  A
+ * candidate IP from a leaf SAN matches when its length equals the
+ * IP half of the subtree and (ip[i] & mask[i]) == (subtree_ip[i] &
+ * mask[i]) for every byte.
+ */
+static int
+ip_in_subtree(uchar *ip, int iplen, GeneralSubtree *st)
+{
+	uchar *base, *mask;
+	int half, i;
+
+	if(st == nil || st->value == nil)
+		return 0;
+	if(st->value->len != 8 && st->value->len != 32)
+		return 0;
+	half = st->value->len / 2;
+	if(iplen != half)
+		return 0;
+	base = st->value->data;
+	mask = st->value->data + half;
+	for(i = 0; i < half; i++)
+		if((ip[i] & mask[i]) != (base[i] & mask[i]))
+			return 0;
+	return 1;
+}
+
+/*
+ * Iterate iPAddress SAN entries (GeneralName form 7) on `c` and
+ * invoke `check(ip, len, ctx)` for each.  IPv4 SAN values are 4
+ * bytes, IPv6 16 bytes; other lengths are silently skipped.
+ */
+static char*
+walk_ip_san(CertX509 *c, char *(*check)(uchar*, int, void*), void *ctx)
+{
+	Elem eext, ealt;
+	Elist *el, *l;
+	Ints *oid;
+	Bytes *altoct, *raw;
+	char *err;
+
+	if(c->ext == nil)
+		return nil;
+	if(decode(c->ext->data, c->ext->len, &eext) != ASN_OK)
+		return nil;
+	err = nil;
+	if(!is_seq(&eext, &el)){
+		freevalfields(&eext.val);
+		return nil;
+	}
+	for(; el != nil && err == nil; el = el->tl){
+		if(!is_seq(&el->hd, &l) || elistlen(l) < 2)
+			continue;
+		if(!is_oid(&l->hd, &oid) || !ints_eq(oid, (Ints*)&oid_subjectAltName))
+			continue;
+		l = l->tl;
+		if(l->hd.val.tag == VBool)
+			l = l->tl;
+		if(l == nil || !is_octetstring(&l->hd, &altoct))
+			continue;
+		if(decode(altoct->data, altoct->len, &ealt) != ASN_OK)
+			continue;
+		if(!is_seq(&ealt, &l)){
+			freevalfields(&ealt.val);
+			continue;
+		}
+		for(; l != nil && err == nil; l = l->tl){
+			if(l->hd.tag.class != Context || l->hd.tag.num != 7)
+				continue;
+			if(l->hd.val.tag != VOctets)
+				continue;
+			raw = l->hd.val.u.octetsval;
+			if(raw->len != 4 && raw->len != 16)
+				continue;
+			err = check(raw->data, raw->len, ctx);
+		}
+		freevalfields(&ealt.val);
+	}
+	freevalfields(&eext.val);
+	return err;
+}
+
+typedef struct ConstraintCtx ConstraintCtx;
+struct ConstraintCtx {
+	GeneralSubtree *permitted;
+	GeneralSubtree *excluded;
+};
+
+static char*
+check_dns_against_constraints(char *name, int nlen, void *vctx)
+{
+	ConstraintCtx *cx;
+	GeneralSubtree *s;
+	int matchedPermitted, sawPermittedDns;
+
+	cx = vctx;
+	for(s = cx->excluded; s != nil; s = s->next){
+		if(s->type != 2)
+			continue;
+		if(dns_in_subtree((char*)s->value->data, s->value->len, name, nlen))
+			return "leaf SAN matches excluded nameConstraints";
+	}
+	sawPermittedDns = 0;
+	matchedPermitted = 0;
+	for(s = cx->permitted; s != nil; s = s->next){
+		if(s->type != 2)
+			continue;
+		sawPermittedDns = 1;
+		if(dns_in_subtree((char*)s->value->data, s->value->len, name, nlen)){
+			matchedPermitted = 1;
+			break;
+		}
+	}
+	if(sawPermittedDns && !matchedPermitted)
+		return "leaf SAN outside permitted nameConstraints";
+	return nil;
+}
+
+static char*
+check_ip_against_constraints(uchar *ip, int iplen, void *vctx)
+{
+	ConstraintCtx *cx;
+	GeneralSubtree *s;
+	int matchedPermitted, sawPermittedIp;
+
+	cx = vctx;
+	for(s = cx->excluded; s != nil; s = s->next){
+		if(s->type != 7)
+			continue;
+		if(ip_in_subtree(ip, iplen, s))
+			return "IP SAN matches excluded nameConstraints";
+	}
+	sawPermittedIp = 0;
+	matchedPermitted = 0;
+	for(s = cx->permitted; s != nil; s = s->next){
+		if(s->type != 7)
+			continue;
+		sawPermittedIp = 1;
+		if(ip_in_subtree(ip, iplen, s)){
+			matchedPermitted = 1;
+			break;
+		}
+	}
+	if(sawPermittedIp && !matchedPermitted)
+		return "IP SAN outside permitted nameConstraints";
+	return nil;
+}
+
+/*
+ * RFC 5280 Section 4.2.1.10 rfc822Name subtree match.  The constraint
+ * is one of:
+ *   "user@host"   exact mailbox match (case-insensitive on host,
+ *                 case-sensitive on local part).
+ *   ".host.dom"   any mailbox whose domain ends with the suffix on
+ *                 a label boundary.
+ *   "host.dom"    any mailbox whose domain equals exactly.
+ */
+static int
+email_in_subtree(char *email, int emaillen, GeneralSubtree *st)
+{
+	char *base, *at, *bat, *dom;
+	int blen, dlen, locallen, blocallen;
+
+	if(st == nil || st->value == nil)
+		return 0;
+	base = (char*)st->value->data;
+	blen = st->value->len;
+	if(blen == 0)
+		return 0;
+	/* find @ in candidate email */
+	for(at = email; at < email + emaillen && *at != '@'; at++)
+		;
+	if(at == email + emaillen)
+		return 0;
+	locallen = at - email;
+	dom = at + 1;
+	dlen = emaillen - locallen - 1;
+	/* case 1: constraint contains '@' -> exact mailbox match */
+	for(bat = base; bat < base + blen && *bat != '@'; bat++)
+		;
+	if(bat < base + blen){
+		blocallen = bat - base;
+		if(blocallen != locallen)
+			return 0;
+		if(strncmp(base, email, locallen) != 0)
+			return 0;
+		if(blen - blocallen - 1 != dlen)
+			return 0;
+		return cistrncmp(bat + 1, dom, dlen) == 0;
+	}
+	/* case 2: constraint begins with '.' -> proper sub-domain.
+	 * Per RFC 5280, ".example.com" matches "foo.example.com" but
+	 * NOT "example.com" itself.  Boundary at dom[dlen-blen] must be
+	 * the '.' that the constraint's leading '.' represents. */
+	if(base[0] == '.'){
+		if(dlen <= blen - 1)
+			return 0;
+		if(dom[dlen - blen] != '.')
+			return 0;
+		return cistrncmp(base + 1, dom + dlen - (blen - 1), blen - 1) == 0;
+	}
+	/* case 3: constraint is a bare host -> exact domain match */
+	if(dlen != blen)
+		return 0;
+	return cistrncmp(base, dom, dlen) == 0;
+}
+
+/*
+ * Iterate rfc822Name SAN entries (GeneralName form 1) on `c` and
+ * invoke `check(email, len, ctx)` for each.
+ */
+static char*
+walk_email_san(CertX509 *c, char *(*check)(char*, int, void*), void *ctx)
+{
+	Elem eext, ealt;
+	Elist *el, *l;
+	Ints *oid;
+	Bytes *altoct, *name;
+	char *err;
+
+	if(c->ext == nil)
+		return nil;
+	if(decode(c->ext->data, c->ext->len, &eext) != ASN_OK)
+		return nil;
+	err = nil;
+	if(!is_seq(&eext, &el)){
+		freevalfields(&eext.val);
+		return nil;
+	}
+	for(; el != nil && err == nil; el = el->tl){
+		if(!is_seq(&el->hd, &l) || elistlen(l) < 2)
+			continue;
+		if(!is_oid(&l->hd, &oid) || !ints_eq(oid, (Ints*)&oid_subjectAltName))
+			continue;
+		l = l->tl;
+		if(l->hd.val.tag == VBool)
+			l = l->tl;
+		if(l == nil || !is_octetstring(&l->hd, &altoct))
+			continue;
+		if(decode(altoct->data, altoct->len, &ealt) != ASN_OK)
+			continue;
+		if(!is_seq(&ealt, &l)){
+			freevalfields(&ealt.val);
+			continue;
+		}
+		for(; l != nil && err == nil; l = l->tl){
+			if(l->hd.tag.class != Context || l->hd.tag.num != 1)
+				continue;
+			if(l->hd.val.tag != VOctets)
+				continue;
+			name = l->hd.val.u.octetsval;
+			err = check((char*)name->data, name->len, ctx);
+		}
+		freevalfields(&ealt.val);
+	}
+	freevalfields(&eext.val);
+	return err;
+}
+
+static char*
+check_email_against_constraints(char *name, int nlen, void *vctx)
+{
+	ConstraintCtx *cx;
+	GeneralSubtree *s;
+	int matchedPermitted, sawPermittedEmail;
+
+	cx = vctx;
+	for(s = cx->excluded; s != nil; s = s->next){
+		if(s->type != 1)
+			continue;
+		if(email_in_subtree(name, nlen, s))
+			return "email SAN matches excluded nameConstraints";
+	}
+	sawPermittedEmail = 0;
+	matchedPermitted = 0;
+	for(s = cx->permitted; s != nil; s = s->next){
+		if(s->type != 1)
+			continue;
+		sawPermittedEmail = 1;
+		if(email_in_subtree(name, nlen, s)){
+			matchedPermitted = 1;
+			break;
+		}
+	}
+	if(sawPermittedEmail && !matchedPermitted)
+		return "email SAN outside permitted nameConstraints";
+	return nil;
+}
+
+/*
+ * Apply accumulated nameConstraints in `cx` to every dNSName,
+ * iPAddress and rfc822Name SAN entry on cert `c`.  Returns first
+ * rejection or nil.  The directoryName form (subject DN matching)
+ * is not enforced: 6b's parser rejects directoryName subtrees as
+ * unsupported via unrecognizedCritical, so a constraint of that
+ * form fails earlier in the verifier.
+ */
+static char*
+apply_constraints_to_cert(CertX509 *c, ConstraintCtx *cx)
+{
+	char *err;
+
+	err = walk_dns_san(c, check_dns_against_constraints, cx);
+	if(err != nil)
+		return err;
+	err = walk_ip_san(c, check_ip_against_constraints, cx);
+	if(err != nil)
+		return err;
+	return walk_email_san(c, check_email_against_constraints, cx);
+}
+
+/*
+ * Per-cert RFC 5280 Section 4.2 capability checks.  isLeaf=1 for the
+ * end-entity cert at the head of the chain, 0 for issuers.  Returns
+ * nil on success, short error string on failure.
+ */
+static char*
+check_cert_caps(CertX509 *c, int isLeaf, int intermediatesBelow)
+{
+	if(c->unrecognizedCritical)
+		return "unrecognized critical extension";
+	if(isLeaf){
+		if(c->hasKeyUsage
+		&& (c->keyUsageBits & (KU_DigitalSignature|KU_KeyEncipherment)) == 0)
+			return "leaf cert lacks digitalSignature/keyEncipherment";
+		if(c->hasExtKeyUsage
+		&& (c->extKeyUsageBits & (EKU_ServerAuth|EKU_AnyKeyUsage)) == 0)
+			return "leaf cert lacks serverAuth EKU";
+		return nil;
+	}
+	if(!c->hasBasicConstraints || !c->isCA)
+		return "non-CA cert in CA position";
+	if(c->hasKeyUsage && (c->keyUsageBits & KU_KeyCertSign) == 0)
+		return "intermediate cert lacks keyCertSign";
+	/* RFC 5280 Section 6.1.4(h)(1): pathLenConstraint is the
+	 * maximum number of non-self-issued intermediates that may
+	 * follow this cert in the path. */
+	if(c->pathLen >= 0 && intermediatesBelow > c->pathLen)
+		return "pathLenConstraint exceeded";
+	return nil;
+}
+
+/*
+ * Prepend cert `c`'s nameConstraints contributions to cx for the
+ * caller's frame to consume.  Allocates fresh GeneralSubtree nodes
+ * (cx is freed with freegeneralsubtrees on the way out, regardless
+ * of how recursion exited).
+ */
+static void
+push_constraints(ConstraintCtx *cx, CertX509 *c)
+{
+	GeneralSubtree *s, *cp;
+
+	if(!c->hasNameConstraints)
+		return;
+	for(s = c->permittedSubtrees; s != nil; s = s->next){
+		cp = (GeneralSubtree*)emalloc(sizeof(GeneralSubtree));
+		cp->next = cx->permitted;
+		cp->type = s->type;
+		cp->value = makebytes(s->value->data, s->value->len);
+		cx->permitted = cp;
+	}
+	for(s = c->excludedSubtrees; s != nil; s = s->next){
+		cp = (GeneralSubtree*)emalloc(sizeof(GeneralSubtree));
+		cp->next = cx->excluded;
+		cp->type = s->type;
+		cp->value = makebytes(s->value->data, s->value->len);
+		cx->excluded = cp;
+	}
+}
+
+/*
  * Recursive DFS path-builder.  `cert` is the cert at the head of the
  * partially-built path (depth 0 = leaf).  Try every entry in `pool`
  * and `roots` whose subject matches cert->issuer and whose key
@@ -3001,6 +3806,14 @@ cert_validity_check(CertX509 *c, long now)
  * detection across cross-signs).  *budget is shared across all
  * branches; when it reaches 0, the search aborts globally.
  *
+ * Per-cert checks (validity, capability) fire on entry against `cert`.
+ * The nameConstraints accumulator `cx` propagates top-down on the
+ * unwind: each frame, on a successful recurse, applies cx (now bearing
+ * contributions from every cert above it in the chain) to its own
+ * SAN, then prepends its own constraints for the caller's frame to
+ * see.  Failed branches roll back their accumulator contributions
+ * before returning so untried candidates see a clean cx.
+ *
  * Returns nil on success.  On failure, returns the most informative
  * error string seen across the explored branches: a fatal error
  * (budget exhausted, depth cap hit) takes priority over the generic
@@ -3010,12 +3823,13 @@ verify_chain_dfs(CertX509 *cert, uchar *cert_pem, int 
 verify_chain_dfs(CertX509 *cert, uchar *cert_pem, int cert_pemlen,
 	PEMChain *pool, PEMChain *roots,
 	PEMChain **visited, int nvisited,
-	int depth, int *budget, long now)
+	int depth, int nonSiDepth, int *budget, long now, ConstraintCtx *cx)
 {
 	PEMChain *cands[2];
 	PEMChain *p;
 	Bytes *cb;
 	CertX509 *parent;
+	GeneralSubtree *s;
 	char *err, *suberr;
 	int i, j, fromroots, seen;
 
@@ -3026,6 +3840,10 @@ verify_chain_dfs(CertX509 *cert, uchar *cert_pem, int 
 	if(err != nil)
 		return err;
 
+	err = check_cert_caps(cert, depth == 0, nonSiDepth);
+	if(err != nil)
+		return err;
+
 	err = "no path to a trust anchor";
 	cands[0] = pool;	/* intermediates first */
 	cands[1] = roots;	/* trust anchors last */
@@ -3064,9 +3882,14 @@ verify_chain_dfs(CertX509 *cert, uchar *cert_pem, int 
 				continue;
 			}
 			if(fromroots){
-				/* Trust anchor reached.  The anchor is not
-				 * subjected to further per-cert checks. */
+				/* Trust anchor reached.  Fold the anchor's
+				 * nameConstraints into cx so they apply to
+				 * every cert below it (RFC 5280 Section 6.1.4
+				 * applies a CA's constraints to descendants;
+				 * the anchor is a CA).  Then add our own. */
+				push_constraints(cx, parent);
 				freecert(parent);
+				push_constraints(cx, cert);
 				return nil;
 			}
 			if(depth + 1 >= MAXHOPS){
@@ -3078,10 +3901,48 @@ verify_chain_dfs(CertX509 *cert, uchar *cert_pem, int 
 			visited[nvisited] = p;
 			suberr = verify_chain_dfs(parent, p->pem, p->pemlen,
 				pool, roots, visited, nvisited+1,
-				depth+1, budget, now);
+				depth + 1,
+				/* RFC 5280 Section 6.1.4(h)(1): pathLen
+				 * counts non-self-issued certs only. */
+				(depth == 0 || cert->isSelfIssued) ? nonSiDepth : nonSiDepth+1,
+				budget, now, cx);
 			freecert(parent);
-			if(suberr == nil)
-				return nil;
+			if(suberr == nil){
+				/* cx now carries every cert above us.
+				 * Apply it to our own SAN. */
+				err = apply_constraints_to_cert(cert, cx);
+				if(err == nil){
+					push_constraints(cx, cert);
+					return nil;
+				}
+				/* Rollback invariant: at frame entry, cx
+				 * holds only the constraints from frames
+				 * above this one.  No code in verify_chain_dfs
+				 * mutates cx BEFORE recursing -- only
+				 * push_constraints after a successful sub-
+				 * recursion.  A future contributor adding
+				 * pre-recursion mutation to cx would silently
+				 * break this rollback.
+				 *
+				 * Roll back every prepend made by frames
+				 * above us so untried candidates see a clean
+				 * cx; the entry state to restore here is
+				 * "empty". */
+				while(cx->permitted != nil){
+					s = cx->permitted;
+					cx->permitted = s->next;
+					freebytes(s->value);
+					free(s);
+				}
+				while(cx->excluded != nil){
+					s = cx->excluded;
+					cx->excluded = s->next;
+					freebytes(s->value);
+					free(s);
+				}
+				/* err carries the apply rejection; keep it. */
+				continue;
+			}
 			/* Budget exhaustion is fatal across all branches. */
 			if(strcmp(suberr, "signature-verify budget exhausted") == 0)
 				return suberr;
@@ -3106,14 +3967,14 @@ verify_chain_dfs(CertX509 *cert, uchar *cert_pem, int 
  *
  * Also calls X509matchhostname on the leaf when `hostname` is non-nil/empty,
  * and checks the validity window (notBefore/notAfter) of every cert traversed
- * during path discovery (RFC 5280 Section 6.1.3(a)(2)).
+ * during path discovery (RFC 5280 Section 6.1.3(a)(2)).  Enforces RFC 5280
+ * Section 4.2 capability constraints: critical-extension recognition,
+ * basicConstraints, keyUsage, extKeyUsage, and nameConstraints (dNSName,
+ * iPAddress, rfc822Name forms; per Section 6.1.4(g) every cert below a
+ * constraint-bearing cert is checked).
  *
  * Bounded by MAXHOPS chain length and MAXVERIFY total signature checks.
  *
- * 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.
  */
 char*
@@ -3123,6 +3984,7 @@ X509verifychain(PEMChain *chain, PEMChain *roots, char
 	Bytes *cb;
 	CertX509 *leaf;
 	PEMChain *visited[MAXHOPS+1];
+	ConstraintCtx cx;
 	int budget;
 	long now;
 
@@ -3146,8 +4008,12 @@ X509verifychain(PEMChain *chain, PEMChain *roots, char
 	now = time(0);
 	visited[0] = chain;
 	budget = MAXVERIFY;
+	cx.permitted = nil;
+	cx.excluded = nil;
 	err = verify_chain_dfs(leaf, chain->pem, chain->pemlen,
-		chain->next, roots, visited, 1, 0, &budget, now);
+		chain->next, roots, visited, 1, 0, 0, &budget, now, &cx);
+	freegeneralsubtrees(cx.permitted);
+	freegeneralsubtrees(cx.excluded);
 	freecert(leaf);
 	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.