--- sys/include/libsec.h
+++ sys/include/libsec.h
@@ -402,6 +402,9 @@ char* X509ecdsaverifydigest(uchar *sig, int siglen, u
char* rsapkcs1verify(RSApub *pk, int sigalg, uchar *msg, ulong msglen, uchar *sig, int siglen);
ECpub* X509toECpub(uchar *cert, int ncert, char *name, int nname, ECdomain *dom);
char* X509ecdsaverifydigest(uchar *sig, int siglen, uchar *edigest, int edigestlen, ECdomain *dom, ECpub *pub);
+char* X509ecdsaverify(uchar *cert, int ncert, ECdomain *dom, ECpub *pub);
+char* X509matchhostname(uchar *cert, int ncert, char *hostname);
+char* X509verifychain(PEMChain *chain, PEMChain *roots, char *hostname);
/*
* elgamal
--- sys/src/libsec/port/x509.c
+++ sys/src/libsec/port/x509.c
@@ -1573,6 +1573,7 @@ typedef struct CertX509 {
Bytes* publickey;
int signature_alg;
Bytes* signature;
+ Bytes* ext; /* raw extensions SEQUENCE (opaque); nil if cert has none */
} CertX509;
/* Algorithm object-ids */
@@ -1691,6 +1692,7 @@ freecert(CertX509* c)
free(c->subject);
freebytes(c->publickey);
freebytes(c->signature);
+ freebytes(c->ext);
free(c);
}
@@ -1827,6 +1829,7 @@ decode_cert(Bytes* a)
c->publickey = nil;
c->signature_alg = -1;
c->signature = nil;
+ c->ext = nil;
/* Certificate */
if(!is_seq(&ecert, &elcert) || elistlen(elcert) !=3)
@@ -1911,6 +1914,18 @@ decode_cert(Bytes* a)
goto errret;
c->publickey = makebytes(bits->data, bits->len);
+ /* optional TBSCertificate extensions: [3] EXPLICIT Extensions */
+ el = el->tl;
+ while(el != nil){
+ if(el->hd.tag.class == Context && el->hd.tag.num == 3
+ && el->hd.val.tag == VOctets){
+ c->ext = el->hd.val.u.octetsval;
+ el->hd.val.u.octetsval = nil; /* steal ownership */
+ break;
+ }
+ el = el->tl;
+ }
+
/*resume Certificate */
if(c->signature_alg < 0)
goto errret;
@@ -2107,19 +2122,25 @@ errret:
return nil;
}
+/*
+ * Convert an ASN.1 INTEGER Elem into a freshly-allocated mpint.
+ * We can't use is_bigint() here -- in the VBigInt case it aliases
+ * pe->val.u.bigintval (no copy), so freeing the returned Bytes would
+ * double-free it when the surrounding ASN tree is later freed.
+ * We read the bytes in place and leave tree ownership alone.
+ */
static mpint*
asn1mpint(Elem *e)
{
Bytes *b;
- mpint *mp;
- int v;
- if(is_int(e, &v))
- return itomp(v, nil);
- if(is_bigint(e, &b)) {
- mp = betomp(b->data, b->len, nil);
- freebytes(b);
- return mp;
+ if(e->tag.class != Universal || e->tag.num != INTEGER)
+ return nil;
+ if(e->val.tag == VInt)
+ return itomp(e->val.u.intval, nil);
+ if(e->val.tag == VBigInt){
+ b = e->val.u.bigintval;
+ return betomp(b->data, b->len, nil);
}
return nil;
}
@@ -2450,7 +2471,7 @@ X509verify(uchar *cert, int ncert, RSApub *pk)
char *e;
Bytes *b;
CertX509 *c;
- uchar digest[SHA1dlen];
+ uchar digest[SHA2_512dlen]; /* sized for any supported hash */
Elem *sigalg;
b = makebytes(cert, ncert);
@@ -2463,6 +2484,473 @@ X509verify(uchar *cert, int ncert, RSApub *pk)
e = verify_signature(c->signature, pk, digest, &sigalg);
freecert(c);
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,
+ SHA2_256dlen, SHA2_384dlen, SHA2_512dlen, SHA2_224dlen,
+ MD5dlen, SHA1dlen, SHA2_256dlen, SHA2_384dlen, SHA2_512dlen, SHA2_224dlen,
+ 0,
+ SHA1dlen, SHA2_256dlen, SHA2_384dlen, SHA2_512dlen,
+ 0,
+};
+
+/*
+ * Verify an X.509 certificate whose TBS is signed with ECDSA.
+ * dom/pub come from the issuer certificate (extract via X509toECpub).
+ */
+char*
+X509ecdsaverify(uchar *cert, int ncert, ECdomain *dom, ECpub *pub)
+{
+ char *e;
+ Bytes *b;
+ CertX509 *c;
+ uchar digest[SHA2_512dlen];
+
+ b = makebytes(cert, ncert);
+ c = decode_cert(b);
+ if(c == nil){
+ freebytes(b);
+ return "cannot decode cert";
+ }
+ if(c->signature_alg < 0
+ || digestalg[c->signature_alg] == nil
+ || digestlen[c->signature_alg] == 0){
+ freecert(c);
+ freebytes(b);
+ return "unsupported signature algorithm";
+ }
+ digest_certinfo(b, digestalg[c->signature_alg], digest);
+ freebytes(b);
+ e = X509ecdsaverifydigest(c->signature->data, c->signature->len,
+ digest, digestlen[c->signature_alg], dom, pub);
+ freecert(c);
+ return e;
+}
+
+/*
+ * RFC 6125 Section 6.4.3: case-insensitive DNS-name match with leftmost-label
+ * wildcard. "*.example.com" matches "foo.example.com" but NOT
+ * "foo.bar.example.com" (one-label wildcard only) or "example.com".
+ * pat is the SAN/CN pattern; host is the caller's expected hostname.
+ * Both assumed to be lowercase-normalized on arrival.
+ */
+static int
+matchhostname(char *pat, int patlen, char *host)
+{
+ int hlen, plen;
+ char *star, *remainder;
+ int prefixlen, suffixlen, i;
+
+ hlen = strlen(host);
+ plen = patlen;
+
+ /* exact match (case-insensitive) */
+ if(plen == hlen && cistrncmp(pat, host, plen) == 0)
+ return 1;
+
+ /* look for a wildcard '*' -- must be in the first label, alone */
+ star = memchr(pat, '*', plen);
+ if(star == nil)
+ return 0;
+ if(star != pat) /* leftmost-label-only wildcard */
+ return 0;
+ if(plen == 1) /* bare '*' pattern matches no host (RFC 6125 Section 6.4.3) */
+ return 0;
+ /* '*' must not span a dot -- it covers exactly one label */
+ remainder = star + 1;
+ if(remainder < pat + plen && *remainder != '.')
+ return 0;
+ /* pat = "*" + remainder; host must have at least one label + remainder */
+ suffixlen = (pat + plen) - remainder;
+ if(hlen < suffixlen + 1) /* need at least 1 char + suffix */
+ return 0;
+ /* the leftmost host label must not contain a dot */
+ for(i = 0; i < hlen - suffixlen; i++){
+ if(host[i] == '.')
+ return 0;
+ }
+ prefixlen = hlen - suffixlen;
+ if(prefixlen < 1)
+ return 0;
+ return cistrncmp(remainder, host + prefixlen, suffixlen) == 0;
+}
+
+/*
+ * Check a certificate's SubjectAltName DNS entries (and fall back to CN
+ * per RFC 6125 Section 6.4.4 only if no DNS SAN is
+ * present) against the expected
+ * hostname. Returns nil on match, error string on failure.
+ */
+/*
+ * Parse a dotted-quad IPv4 literal like "1.2.3.4" into 4 octets.
+ * Returns 4 on success, 0 on parse failure (not a valid IPv4 literal).
+ */
+static int
+parseipv4(char *s, uchar out[4])
+{
+ int i, v;
+ char *p;
+
+ for(i = 0; i < 4; i++){
+ if(*s < '0' || *s > '9')
+ return 0;
+ v = strtol(s, &p, 10);
+ if(p == s || v < 0 || v > 255)
+ return 0;
+ out[i] = v;
+ s = p;
+ if(i < 3){
+ if(*s != '.')
+ return 0;
+ s++;
+ }
+ }
+ return *s == 0 ? 4 : 0;
+}
+
+char*
+X509matchhostname(uchar *cert, int ncert, char *hostname)
+{
+ Bytes *b;
+ CertX509 *c;
+ Elem eext, ealt;
+ Elist *el, *l;
+ Ints *oid;
+ Bytes *altoct, *name;
+ int havesan, matched, iplen, hlen;
+ uchar ipv4[4];
+ char *cn, *err, *hostbuf;
+
+ if(hostname == nil || *hostname == 0)
+ return "no hostname to match against";
+
+ /* RFC 6125 Section 6.4.1: strip trailing dot in reference identifier. */
+ hostbuf = nil;
+ hlen = strlen(hostname);
+ if(hlen > 0 && hostname[hlen-1] == '.'){
+ hostbuf = emalloc(hlen);
+ memmove(hostbuf, hostname, hlen-1);
+ hostbuf[hlen-1] = 0;
+ hostname = hostbuf;
+ }
+
+ /* If hostname is an IPv4 literal, we match iPAddress SAN entries
+ * instead of dNSName entries (RFC 5280
+ * Section 4.2.1.6, RFC 6125 Section 1.7.2). */
+ iplen = parseipv4(hostname, ipv4);
+
+ b = makebytes(cert, ncert);
+ c = decode_cert(b);
+ freebytes(b);
+ if(c == nil){
+ free(hostbuf);
+ return "cannot decode cert";
+ }
+
+ err = "hostname does not match cert";
+ havesan = 0;
+ matched = 0;
+
+ if(c->ext != nil && decode(c->ext->data, c->ext->len, &eext) == ASN_OK){
+ if(is_seq(&eext, &el)){
+ for(; el != nil && !matched; 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;
+ /* skip optional BOOLEAN critical flag */
+ 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; l = l->tl){
+ if(l->hd.tag.class != Context)
+ continue;
+ if(l->hd.val.tag != VOctets)
+ continue;
+ name = l->hd.val.u.octetsval;
+ /* GeneralName [7] iPAddress OCTET STRING -- 4 octets for IPv4 */
+ if(l->hd.tag.num == 7 && iplen == 4){
+ havesan = 1;
+ if(name->len == 4 && memcmp(name->data, ipv4, 4) == 0){
+ matched = 1;
+ break;
+ }
+ continue;
+ }
+ /* GeneralName [2] dNSName IA5String -- only when hostname
+ * is a textual name, not when it's an IP literal. */
+ if(l->hd.tag.num == 2 && iplen == 0){
+ havesan = 1;
+ if(matchhostname((char*)name->data, name->len, hostname)){
+ matched = 1;
+ break;
+ }
+ }
+ }
+ freevalfields(&ealt.val);
+ }
+ }
+ freevalfields(&eext.val);
+ }
+
+ if(!matched && !havesan && iplen == 0){
+ /* Fall back to subject CN (deprecated by RFC 6125 but still widespread).
+ * Only for textual hostnames -- IP literals must have an iPAddress SAN
+ * per RFC 6125 Section 1.7.2; matching CN
+ * text against an IP would be unsafe. */
+ if(c->subject != nil){
+ cn = strchr(c->subject, ',');
+ if(cn != nil)
+ *cn = 0;
+ if(matchhostname(c->subject, strlen(c->subject), hostname))
+ matched = 1;
+ if(cn != nil)
+ *cn = ',';
+ }
+ }
+
+ if(matched)
+ err = nil;
+ freecert(c);
+ free(hostbuf);
+ return err;
+}
+
+/*
+ * Verify that `cert` was signed by `signer` -- extracts the signer's public
+ * key based on its publickey_alg and calls the appropriate algorithm-specific
+ * verify. Handles both RSA and ECDSA issuers. Returns nil on success.
+ */
+static char*
+cert_signedby(uchar *cert, int ncert, uchar *signer, int nsigner)
+{
+ Bytes *sb;
+ CertX509 *sc;
+ RSApub *rsa;
+ ECpub *ec;
+ ECdomain dom;
+ char *err;
+
+ sb = makebytes(signer, nsigner);
+ sc = decode_cert(sb);
+ freebytes(sb);
+ if(sc == nil)
+ return "cannot decode signer cert";
+
+ err = "unsupported signer key algorithm";
+ switch(sc->publickey_alg){
+ case ALG_rsaEncryption:
+ rsa = decode_rsapubkey(sc->publickey);
+ if(rsa == nil){
+ err = "cannot decode signer RSA pubkey";
+ break;
+ }
+ err = X509verify(cert, ncert, rsa);
+ rsapubfree(rsa);
+ break;
+ case ALG_ecPublicKey:
+ if(sc->curve < 0 || sc->curve >= NUMCURVES){
+ err = "unsupported signer EC curve";
+ break;
+ }
+ ecdominit(&dom, namedcurves[sc->curve]);
+ ec = ecdecodepub(&dom, sc->publickey->data, sc->publickey->len);
+ if(ec == nil){
+ ecdomfree(&dom);
+ err = "cannot decode signer EC pubkey";
+ break;
+ }
+ err = X509ecdsaverify(cert, ncert, &dom, ec);
+ ecpubfree(ec);
+ ecdomfree(&dom);
+ break;
+ }
+ freecert(sc);
+ return err;
+}
+
+/*
+ * Path-building budget. MAXHOPS bounds chain length (RFC 5280 implementations
+ * commonly cap at 10). MAXVERIFY caps the number of cryptographic signature
+ * checks across the whole call: a malicious pool full of look-alike subjects
+ * could otherwise force quadratic verify cost. Both are generous for real
+ * Web PKI, where chains are 3-5 deep and pools are small.
+ */
+enum {
+ MAXHOPS = 10,
+ MAXVERIFY = 100,
+};
+
+/*
+ * 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
+ * verifies cert's signature; on a hit from `roots` we have reached a
+ * trust anchor and succeed; on a hit from `pool` we descend with the
+ * discovered intermediate as the new head. On dead-end the recursion
+ * returns to its caller, which tries its next candidate -- this is
+ * the backtracking that distinguishes full DFS from a "first parent
+ * wins" walk.
+ *
+ * `visited[0..nvisited-1]` is the live path (used for cycle
+ * detection across cross-signs). *budget is shared across all
+ * branches; when it reaches 0, the search aborts globally.
+ *
+ * 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
+ * "no path to a trust anchor".
+ */
+static char*
+verify_chain_dfs(CertX509 *cert, uchar *cert_pem, int cert_pemlen,
+ PEMChain *pool, PEMChain *roots,
+ PEMChain **visited, int nvisited,
+ int depth, int *budget)
+{
+ PEMChain *cands[2];
+ PEMChain *p;
+ Bytes *cb;
+ CertX509 *parent;
+ char *err, *suberr;
+ int i, j, fromroots, seen;
+
+ if(*budget <= 0)
+ return "signature-verify budget exhausted";
+
+ err = "no path to a trust anchor";
+ cands[0] = pool; /* intermediates first */
+ cands[1] = roots; /* trust anchors last */
+
+ for(i = 0; i < 2; i++){
+ fromroots = (i == 1);
+ for(p = cands[i]; p != nil; p = p->next){
+ if(p->pem == nil)
+ continue;
+ seen = 0;
+ for(j = 0; j < nvisited; j++)
+ if(visited[j] == p){
+ seen = 1;
+ break;
+ }
+ if(seen)
+ continue;
+ cb = makebytes(p->pem, p->pemlen);
+ parent = decode_cert(cb);
+ freebytes(cb);
+ if(parent == nil)
+ continue;
+ if(parent->subject == nil || cert->issuer == nil
+ || strcmp(parent->subject, cert->issuer) != 0){
+ freecert(parent);
+ continue;
+ }
+ if(*budget <= 0){
+ freecert(parent);
+ return "signature-verify budget exhausted";
+ }
+ (*budget)--;
+ if(cert_signedby(cert_pem, cert_pemlen,
+ p->pem, p->pemlen) != nil){
+ freecert(parent);
+ continue;
+ }
+ if(fromroots){
+ /* Trust anchor reached. The anchor is not
+ * subjected to further per-cert checks. */
+ freecert(parent);
+ return nil;
+ }
+ if(depth + 1 >= MAXHOPS){
+ freecert(parent);
+ if(strcmp(err, "no path to a trust anchor") == 0)
+ err = "chain length exceeds MAXHOPS";
+ continue;
+ }
+ visited[nvisited] = p;
+ suberr = verify_chain_dfs(parent, p->pem, p->pemlen,
+ pool, roots, visited, nvisited+1,
+ depth+1, budget);
+ freecert(parent);
+ if(suberr == nil)
+ return nil;
+ /* Budget exhaustion is fatal across all branches. */
+ if(strcmp(suberr, "signature-verify budget exhausted") == 0)
+ return suberr;
+ err = suberr;
+ }
+ }
+ return err;
+}
+
+/*
+ * Walk a certificate chain. `chain->pem` is the leaf (end-entity cert).
+ * `chain->next..` is treated as an unordered intermediate pool, NOT a fixed
+ * linear path: the verifier finds, for each cert, any candidate in the pool
+ * (or in `roots`) whose subject matches the cert's issuer and whose signature
+ * verifies the cert. Cross-signed paths are accepted: a server may
+ * present a leaf-R3-DST-X3 chain while the client trusts only ISRG-X1,
+ * and R3 may also chain to ISRG-X1 directly.
+ *
+ * Search uses depth-first recursion with backtracking: when multiple
+ * candidate parents are present (e.g. a cross-signed intermediate), each is
+ * tried in turn and a dead-end branch unwinds to try the next candidate.
+ *
+ * Also calls X509matchhostname on the leaf when `hostname` is non-nil/empty.
+ *
+ * Bounded by MAXHOPS chain length and MAXVERIFY total signature checks.
+ *
+ * BUG: does not currently check validity dates, basicConstraints CA:TRUE,
+ * keyUsage/extKeyUsage, or nameConstraints. The cryptographic chain and
+ * hostname binding are enforced.
+ *
+ * Returns nil on success, error string on failure.
+ */
+char*
+X509verifychain(PEMChain *chain, PEMChain *roots, char *hostname)
+{
+ char *err;
+ Bytes *cb;
+ CertX509 *leaf;
+ PEMChain *visited[MAXHOPS+1];
+ int budget;
+
+ if(chain == nil || chain->pem == nil)
+ return "empty chain";
+ if(roots == nil)
+ return "no trust anchors configured";
+
+ if(hostname != nil && *hostname != 0){
+ err = X509matchhostname(chain->pem, chain->pemlen, hostname);
+ if(err != nil)
+ return err;
+ }
+
+ cb = makebytes(chain->pem, chain->pemlen);
+ leaf = decode_cert(cb);
+ freebytes(cb);
+ if(leaf == nil)
+ return "cannot decode leaf cert";
+
+ visited[0] = chain;
+ budget = MAXVERIFY;
+ err = verify_chain_dfs(leaf, chain->pem, chain->pemlen,
+ chain->next, roots, visited, 1, 0, &budget);
+ freecert(leaf);
+ return err;
}
/* ------- Elem constructors ---------- */
|