std1.0.0-beta-7 SafeC docs

Source crypto/x509_parse.sc

View in the repository

// SafeC Standard Library — X.509: full parsing, signature verification and
// host matching (X509Parsed & co. in x509.h).
#pragma once
#include <std/crypto/x509.h>
#include <std/crypto/asn1.h>
#include <std/crypto/digest.h>
#include <std/crypto/rsa.h>
#include <std/crypto/ec.h>

namespace std {

// ── OIDs (DER content bytes) ──────────────────────────────────────────────────
static unsigned char x5_oid_rsa_[9]       = { 0x2a,0x86,0x48,0x86,0xf7,0x0d,0x01,0x01,0x01 };
static unsigned char x5_oid_rsa_pss_[9]   = { 0x2a,0x86,0x48,0x86,0xf7,0x0d,0x01,0x01,0x0a };
static unsigned char x5_oid_sha1rsa_[9]   = { 0x2a,0x86,0x48,0x86,0xf7,0x0d,0x01,0x01,0x05 };
static unsigned char x5_oid_sha256rsa_[9] = { 0x2a,0x86,0x48,0x86,0xf7,0x0d,0x01,0x01,0x0b };
static unsigned char x5_oid_sha384rsa_[9] = { 0x2a,0x86,0x48,0x86,0xf7,0x0d,0x01,0x01,0x0c };
static unsigned char x5_oid_sha512rsa_[9] = { 0x2a,0x86,0x48,0x86,0xf7,0x0d,0x01,0x01,0x0d };
static unsigned char x5_oid_ecpk_[7]      = { 0x2a,0x86,0x48,0xce,0x3d,0x02,0x01 };
static unsigned char x5_oid_p256_[8]      = { 0x2a,0x86,0x48,0xce,0x3d,0x03,0x01,0x07 };
static unsigned char x5_oid_p384_[5]      = { 0x2b,0x81,0x04,0x00,0x22 };
static unsigned char x5_oid_ecdsa1_[7]    = { 0x2a,0x86,0x48,0xce,0x3d,0x04,0x01 };
static unsigned char x5_oid_ecdsa256_[8]  = { 0x2a,0x86,0x48,0xce,0x3d,0x04,0x03,0x02 };
static unsigned char x5_oid_ecdsa384_[8]  = { 0x2a,0x86,0x48,0xce,0x3d,0x04,0x03,0x03 };
static unsigned char x5_oid_ecdsa512_[8]  = { 0x2a,0x86,0x48,0xce,0x3d,0x04,0x03,0x04 };
static unsigned char x5_oid_ed25519_[3]   = { 0x2b,0x65,0x70 };
static unsigned char x5_oid_sha256_[9]    = { 0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x02,0x01 };
static unsigned char x5_oid_sha384_[9]    = { 0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x02,0x02 };
static unsigned char x5_oid_sha512_[9]    = { 0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x02,0x03 };
static unsigned char x5_oid_cn_[3]        = { 0x55,0x04,0x03 };
static unsigned char x5_oid_eku_server_[8] = { 0x2b,0x06,0x01,0x05,0x05,0x07,0x03,0x01 };
static unsigned char x5_oid_eku_client_[8] = { 0x2b,0x06,0x01,0x05,0x05,0x07,0x03,0x02 };
static unsigned char x5_oid_eku_any_[4]   = { 0x55,0x1d,0x25,0x00 };
static unsigned char x5_oid_aia_[8]       = { 0x2b,0x06,0x01,0x05,0x05,0x07,0x01,0x01 };

static struct X509Span x5_span_(const unsigned char* base, const &Asn1_ v) {
    struct X509Span s;
    unsafe { s.off = (unsigned long)v.p - (unsigned long)base; }
    s.len = v.len;
    return s;
}

static struct X509Span x5_none_() {
    struct X509Span s;
    s.off = 0UL;
    s.len = 0UL;
    return s;
}

// ── Time ──────────────────────────────────────────────────────────────────────

static int x5_digits_(const unsigned char* s, unsigned long n, &int out) {
    int v = 0;
    unsafe {
        unsigned long i = 0UL;
        while (i < n) {
            if (s[i] < (unsigned char)'0' || s[i] > (unsigned char)'9') { return 0; }
            v = v * 10 + (int)(s[i] - (unsigned char)'0');
            i = i + 1UL;
        }
    }
    *out = v;
    return 1;
}

// Days since 1970-01-01 of a proleptic Gregorian date (Hinnant's algorithm).
static long long x5_days_(int y, int m, int d) {
    if (m <= 2) { y = y - 1; }
    long long era = (long long)(y >= 0 ? y : y - 399) / 400LL;
    long long yoe = (long long)y - era * 400LL;
    long long mp = (long long)(m > 2 ? m - 3 : m + 9);
    long long doy = (153LL * mp + 2LL) / 5LL + (long long)d - 1LL;
    long long doe = yoe * 365LL + yoe / 4LL - yoe / 100LL + doy;
    return era * 146097LL + doe - 719468LL;
}

static int x5_time_(&Asn1_ in, &unsigned long long out) {
    unsigned char tag = (unsigned char)0;
    struct Asn1_ t;
    if (!asn1_any_(in, &tag, &t, null)) { return 0; }
    int y = 0; int mo = 0; int d = 0; int h = 0; int mi = 0; int s = 0;
    unsafe {
        const unsigned char* p = t.p;
        if (tag == (unsigned char)ASN1_TAG_UTCTIME) {
            if (t.len != 13UL || p[12] != (unsigned char)'Z') { return 0; }
            if (!x5_digits_(p, 2UL, &y)) { return 0; }
            y = y < 50 ? 2000 + y : 1900 + y;
            p = p + 2;
        } else if (tag == (unsigned char)ASN1_TAG_GENTIME) {
            if (t.len != 15UL || p[14] != (unsigned char)'Z') { return 0; }
            if (!x5_digits_(p, 4UL, &y)) { return 0; }
            p = p + 4;
        } else {
            return 0;
        }
        if (!x5_digits_(p, 2UL, &mo) || !x5_digits_(p + 2, 2UL, &d) || !x5_digits_(p + 4, 2UL, &h) ||
            !x5_digits_(p + 6, 2UL, &mi) || !x5_digits_(p + 8, 2UL, &s)) { return 0; }
    }
    if (mo < 1 || mo > 12 || d < 1 || d > 31 || h > 23 || mi > 59 || s > 60) { return 0; }
    long long days = x5_days_(y, mo, d);
    long long secs = days * 86400LL + (long long)h * 3600LL + (long long)mi * 60LL + (long long)s;
    *out = secs < 0LL ? 0ULL : (unsigned long long)secs;
    return 1;
}

// ── Algorithm identifiers ─────────────────────────────────────────────────────

static int x5_hash_of_oid_(const &Asn1_ oid) {
    unsafe {
        if (asn1_oid_is_(oid, &x5_oid_sha256_[0], 9UL)) { return DIGEST_SHA256; }
        if (asn1_oid_is_(oid, &x5_oid_sha384_[0], 9UL)) { return DIGEST_SHA384; }
        if (asn1_oid_is_(oid, &x5_oid_sha512_[0], 9UL)) { return DIGEST_SHA512; }
    }
    return DIGEST_NONE;
}

// RSASSA-PSS-params: [0] hashAlgorithm, [1] maskGenAlgorithm (MGF1 with
// the same hash is all that's accepted), [2] saltLength, [3] trailerField.
static int x5_pss_params_(struct Asn1_ params, &X509Parsed out) {
    struct Asn1_ seq;
    if (!asn1_get_(&params, (unsigned char)ASN1_TAG_SEQUENCE, &seq)) { return 0; }
    int hash = DIGEST_SHA1;
    int salt = 20;
    struct Asn1_ f;
    if (asn1_opt_(&seq, (unsigned char)0xA0, &f) == 1) {
        struct Asn1_ alg;
        struct Asn1_ oid;
        if (!asn1_get_(&f, (unsigned char)ASN1_TAG_SEQUENCE, &alg) || !asn1_get_(&alg, (unsigned char)ASN1_TAG_OID, &oid)) { return 0; }
        hash = x5_hash_of_oid_(&oid);
    }
    if (asn1_opt_(&seq, (unsigned char)0xA1, &f) == 1) {
        struct Asn1_ alg;
        struct Asn1_ oid;
        struct Asn1_ inner;
        struct Asn1_ ioid;
        if (!asn1_get_(&f, (unsigned char)ASN1_TAG_SEQUENCE, &alg) || !asn1_get_(&alg, (unsigned char)ASN1_TAG_OID, &oid)) { return 0; }
        if (!asn1_get_(&alg, (unsigned char)ASN1_TAG_SEQUENCE, &inner) || !asn1_get_(&inner, (unsigned char)ASN1_TAG_OID, &ioid)) { return 0; }
        if (x5_hash_of_oid_(&ioid) != hash) { return 0; }
    }
    if (asn1_opt_(&seq, (unsigned char)0xA2, &f) == 1) {
        if (!asn1_small_int_(&f, &salt)) { return 0; }
    }
    out.sig_hash = hash;
    out.pss_salt = salt;
    return 1;
}

static int x5_sig_alg_(struct Asn1_ alg, &X509Parsed out) {
    struct Asn1_ oid;
    if (!asn1_get_(&alg, (unsigned char)ASN1_TAG_OID, &oid)) { return 0; }
    out.sig_type = X509_SIG_UNKNOWN;
    out.sig_hash = DIGEST_NONE;
    unsafe {
        if (asn1_oid_is_(&oid, &x5_oid_sha256rsa_[0], 9UL)) { out.sig_type = X509_SIG_RSA_PKCS1; out.sig_hash = DIGEST_SHA256; }
        else if (asn1_oid_is_(&oid, &x5_oid_sha384rsa_[0], 9UL)) { out.sig_type = X509_SIG_RSA_PKCS1; out.sig_hash = DIGEST_SHA384; }
        else if (asn1_oid_is_(&oid, &x5_oid_sha512rsa_[0], 9UL)) { out.sig_type = X509_SIG_RSA_PKCS1; out.sig_hash = DIGEST_SHA512; }
        else if (asn1_oid_is_(&oid, &x5_oid_sha1rsa_[0], 9UL)) { out.sig_type = X509_SIG_RSA_PKCS1; out.sig_hash = DIGEST_SHA1; }
        else if (asn1_oid_is_(&oid, &x5_oid_ecdsa256_[0], 8UL)) { out.sig_type = X509_SIG_ECDSA; out.sig_hash = DIGEST_SHA256; }
        else if (asn1_oid_is_(&oid, &x5_oid_ecdsa384_[0], 8UL)) { out.sig_type = X509_SIG_ECDSA; out.sig_hash = DIGEST_SHA384; }
        else if (asn1_oid_is_(&oid, &x5_oid_ecdsa512_[0], 8UL)) { out.sig_type = X509_SIG_ECDSA; out.sig_hash = DIGEST_SHA512; }
        else if (asn1_oid_is_(&oid, &x5_oid_ecdsa1_[0], 7UL)) { out.sig_type = X509_SIG_ECDSA; out.sig_hash = DIGEST_SHA1; }
        else if (asn1_oid_is_(&oid, &x5_oid_ed25519_[0], 3UL)) { out.sig_type = X509_SIG_ED25519; }
        else if (asn1_oid_is_(&oid, &x5_oid_rsa_pss_[0], 9UL)) {
            out.sig_type = X509_SIG_RSA_PSS;
            if (!x5_pss_params_(alg, out)) { return 0; }
        }
    }
    return 1;
}

static int x5_spki_(const unsigned char* base, struct Asn1_ spki, &X509Parsed out) {
    struct Asn1_ alg;
    struct Asn1_ oid;
    struct Asn1_ bits;
    if (!asn1_get_(&spki, (unsigned char)ASN1_TAG_SEQUENCE, &alg)) { return 0; }
    if (!asn1_get_(&alg, (unsigned char)ASN1_TAG_OID, &oid)) { return 0; }
    if (!asn1_get_(&spki, (unsigned char)ASN1_TAG_BIT_STRING, &bits) || bits.len < 1UL) { return 0; }
    unsafe {
        if (bits.p[0] != (unsigned char)0) { return 0; }
        bits.p = bits.p + 1;
        bits.len = bits.len - 1UL;
        out.key = x5_span_(base, &bits);
        out.key_type = X509_KEY_UNKNOWN;
        if (asn1_oid_is_(&oid, &x5_oid_rsa_[0], 9UL) || asn1_oid_is_(&oid, &x5_oid_rsa_pss_[0], 9UL)) {
            out.key_type = X509_KEY_RSA;
        } else if (asn1_oid_is_(&oid, &x5_oid_ecpk_[0], 7UL)) {
            struct Asn1_ curve;
            if (asn1_get_(&alg, (unsigned char)ASN1_TAG_OID, &curve)) {
                if (asn1_oid_is_(&curve, &x5_oid_p256_[0], 8UL)) { out.key_type = X509_KEY_EC_P256; }
                else if (asn1_oid_is_(&curve, &x5_oid_p384_[0], 5UL)) { out.key_type = X509_KEY_EC_P384; }
            }
        } else if (asn1_oid_is_(&oid, &x5_oid_ed25519_[0], 3UL)) {
            out.key_type = X509_KEY_ED25519;
        }
    }
    return 1;
}

static int x5_first_cn_(const unsigned char* base, struct Asn1_ name, &X509Parsed out) {
    struct Asn1_ seq;
    if (!asn1_get_(&name, (unsigned char)ASN1_TAG_SEQUENCE, &seq)) { return 0; }
    while (seq.len > 0UL) {
        struct Asn1_ set;
        if (!asn1_get_(&seq, (unsigned char)ASN1_TAG_SET, &set)) { return 0; }
        while (set.len > 0UL) {
            struct Asn1_ atv;
            struct Asn1_ oid;
            struct Asn1_ val;
            unsigned char tag = (unsigned char)0;
            if (!asn1_get_(&set, (unsigned char)ASN1_TAG_SEQUENCE, &atv)) { return 0; }
            if (!asn1_get_(&atv, (unsigned char)ASN1_TAG_OID, &oid)) { return 0; }
            if (!asn1_any_(&atv, &tag, &val, null)) { return 0; }
            unsafe {
                if (out.subject_cn.len == 0UL && asn1_oid_is_(&oid, &x5_oid_cn_[0], 3UL) &&
                    (tag == (unsigned char)ASN1_TAG_UTF8 || tag == (unsigned char)ASN1_TAG_PRINTABLE ||
                     tag == (unsigned char)ASN1_TAG_IA5 || tag == (unsigned char)ASN1_TAG_T61)) {
                    out.subject_cn = x5_span_(base, &val);
                }
            }
        }
    }
    return 1;
}

static int x5_extensions_(const unsigned char* base, struct Asn1_ exts, &X509Parsed out) {
    struct Asn1_ seq;
    if (!asn1_get_(&exts, (unsigned char)ASN1_TAG_SEQUENCE, &seq)) { return 0; }
    while (seq.len > 0UL) {
        struct Asn1_ ext;
        struct Asn1_ oid;
        struct Asn1_ val;
        struct Asn1_ crit;
        int critical = 0;
        if (!asn1_get_(&seq, (unsigned char)ASN1_TAG_SEQUENCE, &ext)) { return 0; }
        if (!asn1_get_(&ext, (unsigned char)ASN1_TAG_OID, &oid)) { return 0; }
        int r = asn1_opt_(&ext, (unsigned char)ASN1_TAG_BOOLEAN, &crit);
        if (r < 0) { return 0; }
        unsafe { if (r == 1 && crit.len == 1UL && crit.p[0] != (unsigned char)0) { critical = 1; } }
        if (!asn1_get_(&ext, (unsigned char)ASN1_TAG_OCTET_STRING, &val)) { return 0; }
        int known = 0;
        unsafe {
            if (oid.len == 3UL && oid.p[0] == (unsigned char)0x55 && oid.p[1] == (unsigned char)0x1d) {
                unsigned char id = oid.p[2];
                if (id == (unsigned char)0x11) {             // subjectAltName
                    struct Asn1_ names;
                    if (!asn1_get_(&val, (unsigned char)ASN1_TAG_SEQUENCE, &names)) { return 0; }
                    out.san = x5_span_(base, &names);
                    known = 1;
                } else if (id == (unsigned char)0x13) {      // basicConstraints
                    struct Asn1_ bc;
                    if (!asn1_get_(&val, (unsigned char)ASN1_TAG_SEQUENCE, &bc)) { return 0; }
                    out.has_basic_constraints = 1;
                    struct Asn1_ ca;
                    int rr = asn1_opt_(&bc, (unsigned char)ASN1_TAG_BOOLEAN, &ca);
                    if (rr < 0) { return 0; }
                    if (rr == 1 && ca.len == 1UL && ca.p[0] != (unsigned char)0) { out.is_ca = 1; }
                    if (bc.len > 0UL) {
                        int pl = 0;
                        if (!asn1_small_int_(&bc, &pl)) { return 0; }
                        out.path_len = pl;
                    }
                    known = 1;
                } else if (id == (unsigned char)0x0f) {      // keyUsage
                    struct Asn1_ ku;
                    if (!asn1_get_(&val, (unsigned char)ASN1_TAG_BIT_STRING, &ku) || ku.len < 2UL) { return 0; }
                    out.has_key_usage = 1;
                    out.key_usage = (int)ku.p[1];
                    if (ku.len > 2UL) { out.key_usage = out.key_usage | ((int)ku.p[2] << 8); }
                    known = 1;
                } else if (id == (unsigned char)0x25) {      // extKeyUsage
                    struct Asn1_ list;
                    if (!asn1_get_(&val, (unsigned char)ASN1_TAG_SEQUENCE, &list)) { return 0; }
                    out.has_eku = 1;
                    while (list.len > 0UL) {
                        struct Asn1_ k;
                        if (!asn1_get_(&list, (unsigned char)ASN1_TAG_OID, &k)) { return 0; }
                        if (asn1_oid_is_(&k, &x5_oid_eku_server_[0], 8UL)) { out.eku_server = 1; }
                        if (asn1_oid_is_(&k, &x5_oid_eku_client_[0], 8UL)) { out.eku_client = 1; }
                        if (asn1_oid_is_(&k, &x5_oid_eku_any_[0], 4UL)) { out.eku_any = 1; }
                    }
                    known = 1;
                } else if (id == (unsigned char)0x1e) {      // nameConstraints: noted, not processed
                    out.has_name_constraints = 1;
                } else if (id == (unsigned char)0x20 || id == (unsigned char)0x24 || id == (unsigned char)0x36 ||
                           id == (unsigned char)0x23 || id == (unsigned char)0x0e || id == (unsigned char)0x1f) {
                    // certificatePolicies, policyConstraints, inhibitAnyPolicy,
                    // authority/subject key ids, CRL distribution points: no
                    // effect on a path validated without policy processing.
                    known = 1;
                }
            } else if (asn1_oid_is_(&oid, &x5_oid_aia_[0], 8UL)) {
                known = 1;
            }
        }
        if (critical && !known) { out.unknown_critical = 1; }
    }
    return 1;
}

int x509_parse_n_(const &unsigned char der, unsigned long len, &X509Parsed out) {
    unsafe {
        const unsigned char* base = (const unsigned char*)der;
        out.der_len = len;
        out.tbs = x5_none_(); out.serial = x5_none_(); out.issuer = x5_none_(); out.subject = x5_none_();
        out.spki = x5_none_(); out.key = x5_none_(); out.sig = x5_none_(); out.san = x5_none_();
        out.subject_cn = x5_none_();
        out.version = 1; out.key_type = X509_KEY_UNKNOWN; out.sig_type = X509_SIG_UNKNOWN; out.sig_hash = DIGEST_NONE;
        out.pss_salt = 0; out.not_before = 0ULL; out.not_after = 0ULL;
        out.has_basic_constraints = 0; out.is_ca = 0; out.path_len = -1; out.has_key_usage = 0; out.key_usage = 0;
        out.has_eku = 0; out.eku_server = 0; out.eku_client = 0; out.eku_any = 0;
        out.has_name_constraints = 0; out.unknown_critical = 0;

        struct Asn1_ in = asn1_view_(base, len);
        struct Asn1_ cert;
        if (!asn1_get_(&in, (unsigned char)ASN1_TAG_SEQUENCE, &cert) || in.len != 0UL) { return X509_ERR_MALFORMED; }
        struct Asn1_ tbsWhole;
        struct Asn1_ tbs;
        unsigned char tag = (unsigned char)0;
        if (!asn1_any_(&cert, &tag, &tbs, &tbsWhole) || tag != (unsigned char)ASN1_TAG_SEQUENCE) { return X509_ERR_MALFORMED; }
        out.tbs = x5_span_(base, &tbsWhole);
        struct Asn1_ outerAlg;
        struct Asn1_ outerAlgWhole;
        if (!asn1_any_(&cert, &tag, &outerAlg, &outerAlgWhole) || tag != (unsigned char)ASN1_TAG_SEQUENCE) { return X509_ERR_MALFORMED; }
        struct Asn1_ sigBits;
        if (!asn1_get_(&cert, (unsigned char)ASN1_TAG_BIT_STRING, &sigBits) || sigBits.len < 1UL ||
            sigBits.p[0] != (unsigned char)0 || cert.len != 0UL) { return X509_ERR_MALFORMED; }
        sigBits.p = sigBits.p + 1;
        sigBits.len = sigBits.len - 1UL;
        out.sig = x5_span_(base, &sigBits);
        if (!x5_sig_alg_(outerAlg, out)) { return X509_ERR_MALFORMED; }

        // TBSCertificate
        struct Asn1_ v;
        int r = asn1_opt_(&tbs, (unsigned char)0xA0, &v);
        if (r < 0) { return X509_ERR_MALFORMED; }
        if (r == 1) {
            int ver = 0;
            if (!asn1_small_int_(&v, &ver) || ver > 2) { return X509_ERR_MALFORMED; }
            out.version = ver + 1;
        }
        struct Asn1_ serial;
        if (!asn1_get_(&tbs, (unsigned char)ASN1_TAG_INTEGER, &serial)) { return X509_ERR_MALFORMED; }
        out.serial = x5_span_(base, &serial);
        struct Asn1_ innerAlg;
        struct Asn1_ innerAlgWhole;
        if (!asn1_any_(&tbs, &tag, &innerAlg, &innerAlgWhole) || tag != (unsigned char)ASN1_TAG_SEQUENCE) { return X509_ERR_MALFORMED; }
        if (!asn1_eq_(&innerAlgWhole, &outerAlgWhole)) { return X509_ERR_MALFORMED; }
        struct Asn1_ issuer;
        struct Asn1_ issuerWhole;
        if (!asn1_any_(&tbs, &tag, &issuer, &issuerWhole) || tag != (unsigned char)ASN1_TAG_SEQUENCE) { return X509_ERR_MALFORMED; }
        out.issuer = x5_span_(base, &issuerWhole);
        struct Asn1_ validity;
        if (!asn1_get_(&tbs, (unsigned char)ASN1_TAG_SEQUENCE, &validity)) { return X509_ERR_MALFORMED; }
        if (!x5_time_(&validity, &out.not_before) || !x5_time_(&validity, &out.not_after)) { return X509_ERR_MALFORMED; }
        struct Asn1_ subject;
        struct Asn1_ subjectWhole;
        if (!asn1_any_(&tbs, &tag, &subject, &subjectWhole) || tag != (unsigned char)ASN1_TAG_SEQUENCE) { return X509_ERR_MALFORMED; }
        out.subject = x5_span_(base, &subjectWhole);
        if (!x5_first_cn_(base, subjectWhole, out)) { return X509_ERR_MALFORMED; }
        struct Asn1_ spki;
        struct Asn1_ spkiWhole;
        if (!asn1_any_(&tbs, &tag, &spki, &spkiWhole) || tag != (unsigned char)ASN1_TAG_SEQUENCE) { return X509_ERR_MALFORMED; }
        out.spki = x5_span_(base, &spkiWhole);
        if (!x5_spki_(base, spki, out)) { return X509_ERR_MALFORMED; }
        // issuerUniqueID [1], subjectUniqueID [2] (skipped), extensions [3]
        while (tbs.len > 0UL) {
            struct Asn1_ f;
            if (!asn1_any_(&tbs, &tag, &f, null)) { return X509_ERR_MALFORMED; }
            if (tag == (unsigned char)0xA3) {
                if (!x5_extensions_(base, f, out)) { return X509_ERR_MALFORMED; }
            } else if (tag != (unsigned char)0x81 && tag != (unsigned char)0x82) {
                return X509_ERR_MALFORMED;
            }
        }
    }
    return 0;
}

int x509_parse([]const unsigned char der_s, &X509Parsed out) {
    unsigned long len = der_s.len;
    const &unsigned char der = crypto_in_(der_s, 0UL);
    return x509_parse_n_(der, len, out);
}

static struct Asn1_ x5_view_(const &unsigned char der, const &X509Span s) {
    unsafe { return asn1_view_((const unsigned char*)der + s.off, s.len); }
}

int x509_issued_by_n_(const &unsigned char der, const &X509Parsed c,
                   const &unsigned char issuer_der, const &X509Parsed issuer) {
    struct Asn1_ a = x5_view_(der, &c.issuer);
    struct Asn1_ b = x5_view_(issuer_der, &issuer.subject);
    return asn1_eq_(&a, &b);
}

int x509_issued_by([]const unsigned char der_s, const &X509Parsed c, []const unsigned char issuer_der_s, const &X509Parsed issuer) {
    const &unsigned char der = crypto_in_(der_s, 0UL);
    const &unsigned char issuer_der = crypto_in_(issuer_der_s, 0UL);
    return x509_issued_by_n_(der, c, issuer_der, issuer);
}

int x509_verify_signed_by_n_(const &unsigned char der, const &X509Parsed c,
                          const &unsigned char issuer_der, const &X509Parsed issuer) {
    if (c.sig_hash == DIGEST_NONE || c.sig_hash == DIGEST_SHA1) { return 0; }
    unsigned char h[DIGEST_MAX_SIZE];
    unsigned long hl = 0UL;
    unsafe { hl = digest_n_(c.sig_hash, (const unsigned char*)der + c.tbs.off, c.tbs.len, &h[0]); }
    if (hl == 0UL) { return 0; }
    unsafe {
        const unsigned char* sig = (const unsigned char*)der + c.sig.off;
        const unsigned char* key = (const unsigned char*)issuer_der + issuer.key.off;
        if (c.sig_type == X509_SIG_RSA_PKCS1 || c.sig_type == X509_SIG_RSA_PSS) {
            if (issuer.key_type != X509_KEY_RSA) { return 0; }
            struct RsaPublicKey k;
            if (!k.init_der_n_((const &unsigned char)key, issuer.key.len)) { return 0; }
            if (c.sig_type == X509_SIG_RSA_PKCS1) {
                return rsa_verify_pkcs1_n_(&k, c.sig_hash, &h[0], hl, (const &unsigned char)sig, c.sig.len);
            }
            if (c.pss_salt < 0) { return 0; }
            return rsa_verify_pss_n_(&k, c.sig_hash, &h[0], hl, (const &unsigned char)sig, c.sig.len, (unsigned long)c.pss_salt);
        }
        if (c.sig_type == X509_SIG_ECDSA) {
            int curve = 0;
            if (issuer.key_type == X509_KEY_EC_P256) { curve = EC_P256; }
            else if (issuer.key_type == X509_KEY_EC_P384) { curve = EC_P384; }
            else { return 0; }
            return ecdsa_verify_der_n_(curve, (const &unsigned char)key, issuer.key.len, &h[0], hl,
                                    (const &unsigned char)sig, c.sig.len);
        }
    }
    return 0;
}

int x509_verify_signed_by([]const unsigned char der_s, const &X509Parsed c, []const unsigned char issuer_der_s, const &X509Parsed issuer) {
    const &unsigned char der = crypto_in_(der_s, 0UL);
    const &unsigned char issuer_der = crypto_in_(issuer_der_s, 0UL);
    return x509_verify_signed_by_n_(der, c, issuer_der, issuer);
}

// ── Host matching ─────────────────────────────────────────────────────────────

static unsigned char x5_lower_(unsigned char c) {
    if (c >= (unsigned char)'A' && c <= (unsigned char)'Z') { return (unsigned char)(c + (unsigned char)32); }
    return c;
}

// Parses an IPv4 dotted quad into 4 bytes.
static int x5_ipv4_(const char* s, unsigned long n, unsigned char* out) {
    int part = 0;
    int val = -1;
    unsafe {
        unsigned long i = 0UL;
        while (i <= n) {
            char ch = i < n ? s[i] : '.';
            if (ch == '.') {
                if (val < 0 || part > 3) { return 0; }
                out[part] = (unsigned char)val;
                part = part + 1;
                val = -1;
            } else if (ch >= '0' && ch <= '9') {
                val = (val < 0 ? 0 : val * 10) + (int)(ch - '0');
                if (val > 255) { return 0; }
            } else {
                return 0;
            }
            i = i + 1UL;
        }
    }
    return part == 4;
}

static int x5_hexv_(char c) {
    if (c >= '0' && c <= '9') { return (int)(c - '0'); }
    if (c >= 'a' && c <= 'f') { return (int)(c - 'a') + 10; }
    if (c >= 'A' && c <= 'F') { return (int)(c - 'A') + 10; }
    return -1;
}

// Parses an IPv6 literal (optionally in brackets, '::' compression, no
// embedded IPv4 or zone) into 16 bytes.
static int x5_ipv6_(const char* s, unsigned long n, unsigned char* out) {
    unsigned int groups[8];
    int count = 0;
    int gap = -1;
    unsafe {
        if (n >= 2UL && s[0] == '[' && s[n - 1UL] == ']') { s = s + 1; n = n - 2UL; }
        unsigned long i = 0UL;
        if (n >= 2UL && s[0] == ':' && s[1] == ':') { gap = 0; i = 2UL; }
        while (i < n) {
            unsigned int v = 0U;
            int digits = 0;
            while (i < n && x5_hexv_(s[i]) >= 0) {
                v = (v << 4) | (unsigned int)x5_hexv_(s[i]);
                digits = digits + 1;
                i = i + 1UL;
            }
            if (digits == 0 || digits > 4 || count >= 8) { return 0; }
            groups[count] = v;
            count = count + 1;
            if (i == n) { break; }
            if (s[i] != ':') { return 0; }
            i = i + 1UL;
            if (i < n && s[i] == ':') {
                if (gap >= 0) { return 0; }
                gap = count;
                i = i + 1UL;
            } else if (i == n) {
                return 0;
            }
        }
    }
    if (gap < 0 && count != 8) { return 0; }
    if (gap >= 0 && count > 7) { return 0; }
    unsafe {
        int k = 0;
        while (k < 16) { out[k] = (unsigned char)0; k = k + 1; }
        int g = 0;
        int pos = 0;
        while (g < count) {
            if (g == gap) { pos = pos + (8 - count); }
            out[pos * 2] = (unsigned char)(groups[g] >> 8);
            out[pos * 2 + 1] = (unsigned char)(groups[g] & 0xFFU);
            pos = pos + 1;
            g = g + 1;
        }
    }
    return 1;
}

// DNS name (pattern p, len pn) against host h (len hn, lowercased, no
// trailing dot). A leading "*." matches exactly one non-empty label, and
// only with at least two more labels after it.
static int x5_dns_match_(const unsigned char* p, unsigned long pn, const unsigned char* h, unsigned long hn) {
    unsafe {
        if (pn > 0UL && p[pn - 1UL] == (unsigned char)'.') { pn = pn - 1UL; }
        if (pn == 0UL) { return 0; }
        if (pn >= 2UL && p[0] == (unsigned char)'*' && p[1] == (unsigned char)'.') {
            const unsigned char* rest = p + 1;      // ".example.com"
            unsigned long rn = pn - 1UL;
            unsigned long dots = 0UL;
            unsigned long i = 1UL;
            while (i < rn) {
                if (rest[i] == (unsigned char)'*') { return 0; }
                if (rest[i] == (unsigned char)'.') { dots = dots + 1UL; }
                i = i + 1UL;
            }
            if (dots < 1UL) { return 0; }
            // the host's first label, then the same suffix
            unsigned long first = 0UL;
            while (first < hn && h[first] != (unsigned char)'.') { first = first + 1UL; }
            if (first == 0UL || first == hn) { return 0; }
            if (hn - first != rn) { return 0; }
            i = 0UL;
            while (i < rn) {
                if (x5_lower_(rest[i]) != h[first + i]) { return 0; }
                i = i + 1UL;
            }
            return 1;
        }
        if (pn != hn) { return 0; }
        unsigned long i = 0UL;
        while (i < pn) {
            if (p[i] == (unsigned char)'*') { return 0; }
            if (x5_lower_(p[i]) != h[i]) { return 0; }
            i = i + 1UL;
        }
    }
    return 1;
}

int x509_match_host_n_(const &unsigned char der, const &X509Parsed c, const &char host) {
    unsigned char h[256];
    unsigned long hn = 0UL;
    unsafe {
        const char* s = (const char*)host;
        while (s[hn] != (char)0) {
            if (hn >= 255UL) { return 0; }
            h[hn] = x5_lower_((unsigned char)s[hn]);
            hn = hn + 1UL;
        }
    }
    if (hn == 0UL) { return 0; }
    if (h[hn - 1UL] == (unsigned char)'.') { hn = hn - 1UL; }
    unsigned char ip[16];
    unsigned long ipLen = 0UL;
    unsafe {
        if (x5_ipv4_((const char*)&h[0], hn, &ip[0])) { ipLen = 4UL; }
        else if (x5_ipv6_((const char*)&h[0], hn, &ip[0])) { ipLen = 16UL; }
    }
    int sawName = 0;
    if (c.san.len > 0UL) {
        struct Asn1_ names = x5_view_(der, &c.san);
        while (names.len > 0UL) {
            unsigned char tag = (unsigned char)0;
            struct Asn1_ gn;
            if (!asn1_any_(&names, &tag, &gn, null)) { return 0; }
            unsafe {
                if (tag == (unsigned char)0x82) {
                    sawName = 1;
                    if (ipLen == 0UL && x5_dns_match_(gn.p, gn.len, (const unsigned char*)&h[0], hn)) { return 1; }
                } else if (tag == (unsigned char)0x87) {
                    sawName = 1;
                    if (ipLen != 0UL && gn.len == ipLen) {
                        unsigned long i = 0UL;
                        int same = 1;
                        while (i < ipLen) { if (gn.p[i] != ip[i]) { same = 0; } i = i + 1UL; }
                        if (same) { return 1; }
                    }
                }
            }
        }
    }
    if (sawName || ipLen != 0UL || c.subject_cn.len == 0UL) { return 0; }
    unsafe {
        return x5_dns_match_((const unsigned char*)der + c.subject_cn.off, c.subject_cn.len, (const unsigned char*)&h[0], hn);
    }
}

int x509_match_host([]const unsigned char der_s, const &X509Parsed c, const &char host) {
    const &unsigned char der = crypto_in_(der_s, 0UL);
    return x509_match_host_n_(der, c, host);
}

} // namespace std