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