Skip to content

Certificate Decoder — C source

Paste a PEM X.509 certificate and see its subject, issuer, SAN, validity, key usage, fingerprints, and chain details in a human-readable format.

This is the C implementation — the same logic the interactive tool runs, in a shareable, citable form.

/*
 * cert-decoder — pure ASN.1 DER parser + X.509 certificate decoder.
 *
 * Language: C (C11, POSIX)
 * Source:   CosmoDev polyglot showcase port of the Certificate Decoder tool,
 *           ported from src/lib/cert-decoder.ts (the canonical TypeScript
 *           implementation).
 * License:  display source — part of CosmoDev's polyglot tool pages.
 *
 * DER is deterministic and parsed sequentially, so the TS reference needs no
 * external dependencies — and neither does C. The OpenSSL library would do
 * this too (d2i_X509), but the point of this port is the walk itself: every
 * TLV, OID, RDN, UTCTime and extension bit is decoded by hand below, exactly
 * like the reference.
 *
 * Ownership: decode_certificate() fills a certificate_info of heap
 * allocations — release it with certificate_info_free(). Functions return
 * NULL on success or a static error message (the TS reference throws Error).
 *
 * Build: cc -std=c11 cert-decoder.c
 */

#define _POSIX_C_SOURCE 200809L

#include <ctype.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

/* --------------------------------------------------------------- strings --- */

/** Growable list of heap strings. */
typedef struct {
    char **items;
    size_t count;
} str_list;

static bool str_list_push(str_list *l, const char *s) {
    char **grown = realloc(l->items, (l->count + 1) * sizeof *grown);
    if (!grown) return false;
    l->items = grown;
    l->items[l->count] = strdup(s);
    l->count++;
    return l->items[l->count - 1] != NULL;
}

static void str_list_free(str_list *l) {
    for (size_t i = 0; i < l->count; i++) free(l->items[i]);
    free(l->items);
    l->items = NULL;
    l->count = 0;
}

/* ------------------------------------------------------------ OID tables --- */

typedef struct {
    const char *oid;
    const char *name;
} oid_name_entry;

static const oid_name_entry OID_NAMES[] = {
    {"1.2.840.113549.1.1.1", "RSA"},
    {"1.2.840.113549.1.1.5", "SHA-1 with RSA"},
    {"1.2.840.113549.1.1.11", "SHA-256 with RSA"},
    {"1.2.840.113549.1.1.12", "SHA-384 with RSA"},
    {"1.2.840.113549.1.1.13", "SHA-512 with RSA"},
    {"1.2.840.113549.1.1.14", "SHA-224 with RSA"},
    {"1.2.840.10045.2.1", "ECDSA"},
    {"1.2.840.10045.4.3.2", "ECDSA with SHA-256"},
    {"1.2.840.10045.4.3.3", "ECDSA with SHA-384"},
    {"1.2.840.10045.4.3.4", "ECDSA with SHA-512"},
    {"1.3.14.3.2.29", "SHA-1 with RSA (OIW)"},
    {"2.5.4.3", "CN"},
    {"2.5.4.6", "C"},
    {"2.5.4.7", "L"},
    {"2.5.4.8", "ST"},
    {"2.5.4.10", "O"},
    {"2.5.4.11", "OU"},
    {"2.5.29.14", "Subject Key Identifier"},
    {"2.5.29.15", "Key Usage"},
    {"2.5.29.17", "Subject Alternative Name"},
    {"2.5.29.19", "Basic Constraints"},
    {"2.5.29.35", "Authority Key Identifier"},
    {"2.5.29.37", "Extended Key Usage"},
    {"1.3.6.1.5.5.7.1.1", "Authority Information Access"},
    {"1.3.6.1.5.5.7.3.1", "serverAuth"},
    {"1.3.6.1.5.5.7.3.2", "clientAuth"},
    {"1.3.6.1.5.5.7.3.3", "codeSigning"},
    {"1.3.6.1.5.5.7.3.4", "emailProtection"},
    {"1.3.6.1.5.5.7.3.8", "timeStamping"},
    {"1.2.840.113549.1.9.14", "Extension Request"},
    {"1.2.840.113549.1.9.1", "emailAddress"},
    {"1.3.6.1.5.5.7.1.3", "CRL Distribution Points"},
    {"1.3.6.1.4.1.11129.2.1.17", "CT Precertificate SCTs"},
    {"1.3.6.1.4.1.311.21.7", "Microsoft Certificate Template"},
};

static const oid_name_entry EC_CURVE_NAMES[] = {
    {"1.2.840.10045.3.1.7", "P-256"},
    {"1.3.132.0.34", "P-384"},
    {"1.3.132.0.35", "P-512"},
    {"1.3.132.0.10", "secp256k1"},
};

static const char *oid_table_lookup(const oid_name_entry *table, size_t n, const char *oid) {
    for (size_t i = 0; i < n; i++) {
        if (strcmp(table[i].oid, oid) == 0) return table[i].name;
    }
    return NULL;
}

/** The OID's short name, or the dotted OID itself when unknown. */
static const char *oid_name(const char *oid) {
    const char *name = oid_table_lookup(OID_NAMES, sizeof OID_NAMES / sizeof OID_NAMES[0], oid);
    return name ? name : oid;
}

/* Key Usage bit names. */
static const char *KEY_USAGE_BITS[] = {
    "digitalSignature", "nonRepudiation", "keyEncipherment", "dataEncipherment",
    "keyAgreement", "keyCertSign", "cRLSign", "encipherOnly", "decipherOnly",
};

/* ----------------------------------------------------------- ASN.1 nodes --- */

typedef enum { ASN1_UNIVERSAL, ASN1_APPLICATION, ASN1_CONTEXT, ASN1_PRIVATE } asn1_class;

typedef struct asn1_node {
    asn1_class klass;
    bool constructed;
    long tag_number;
    uint8_t *value;     /* owned copy of the value bytes */
    size_t value_len;
    size_t offset;      /* offset into the original buffer */
    struct asn1_node **children;
    size_t child_count;
} asn1_node;

static void asn1_free(asn1_node *node) {
    if (!node) return;
    for (size_t i = 0; i < node->child_count; i++) asn1_free(node->children[i]);
    free(node->children);
    free(node->value);
    free(node);
}

static asn1_node *asn1_new(void) { return calloc(1, sizeof(asn1_node)); }

static bool asn1_add_child(asn1_node *parent, asn1_node *child) {
    asn1_node **grown = realloc(parent->children, (parent->child_count + 1) * sizeof *grown);
    if (!grown) return false;
    parent->children = grown;
    parent->children[parent->child_count++] = child;
    return true;
}

/* --------------------------------------------------------- DER traversal --- */

typedef struct {
    const uint8_t *buf;
    size_t len;
    size_t pos;
} der_reader;

/** Read the next TLV node; NULL (with *err set) on malformed DER. */
static asn1_node *der_read_node(der_reader *r, const char **err) {
    size_t offset = r->pos;
    if (r->pos >= r->len) { *err = "Unexpected end of DER data"; return NULL; }
    uint8_t byte0 = r->buf[r->pos++];

    asn1_node *node = asn1_new();
    if (!node) { *err = "out of memory"; return NULL; }
    node->offset = offset;
    node->klass = (asn1_class)((byte0 & 0xc0) >> 6);
    node->constructed = (byte0 & 0x20) != 0;
    long tag = byte0 & 0x1f;

    /* Long-form tag (tag number >= 31) */
    if (tag == 0x1f) {
        tag = 0;
        uint8_t b;
        do {
            if (r->pos >= r->len) { *err = "Unexpected end of DER data"; goto fail; }
            b = r->buf[r->pos++];
            tag = (tag << 7) | (b & 0x7f);
        } while (b & 0x80);
    }
    node->tag_number = tag;

    /* Length */
    if (r->pos >= r->len) { *err = "Unexpected end of DER data"; goto fail; }
    uint8_t first = r->buf[r->pos++];
    size_t length;
    if (first < 0x80) {
        length = first;
    } else {
        size_t num_bytes = first & 0x7f;
        if (num_bytes == 0) { *err = "Indefinite length is not supported in DER"; goto fail; }
        if (num_bytes > 4) {
            static char msg[64];
            snprintf(msg, sizeof msg, "Length too large: %zu bytes", num_bytes);
            *err = msg;
            goto fail;
        }
        length = 0;
        for (size_t i = 0; i < num_bytes; i++) {
            if (r->pos >= r->len) { *err = "Unexpected end of DER data"; goto fail; }
            length = (length << 8) | r->buf[r->pos++];
        }
    }

    if (r->pos + length > r->len) {
        static char msg[96];
        snprintf(msg, sizeof msg, "Truncated DER: need %zu bytes at offset %zu, but only %zu remain",
                 length, r->pos, r->len - r->pos);
        *err = msg;
        goto fail;
    }

    node->value = malloc(length ? length : 1);
    if (!node->value) { *err = "out of memory"; goto fail; }
    memcpy(node->value, r->buf + r->pos, length);
    node->value_len = length;
    r->pos += length;

    /* Parse children for constructed types */
    if (node->constructed && node->value_len > 0) {
        der_reader child = {node->value, node->value_len, 0};
        while (child.pos < child.len) {
            asn1_node *c = der_read_node(&child, err);
            if (!c) goto fail;
            if (!asn1_add_child(node, c)) { asn1_free(c); *err = "out of memory"; goto fail; }
        }
    }
    return node;

fail:
    asn1_free(node);
    return NULL;
}

/* ------------------------------------------------------------ OID decode --- */

/** OID content bytes -> dotted string. Returns false on an empty OID. */
static bool decode_oid(const uint8_t *bytes, size_t len, char out[128]) {
    if (len == 0) return false;
    long parts[64];
    size_t nparts = 0;
    parts[nparts++] = bytes[0] / 40;
    parts[nparts++] = bytes[0] % 40;
    unsigned long value = 0;
    for (size_t i = 1; i < len && nparts < 64; i++) {
        uint8_t b = bytes[i];
        value = (value << 7) | (b & 0x7f);
        if ((b & 0x80) == 0) {
            parts[nparts++] = (long)value;
            value = 0;
        }
    }
    out[0] = 0;
    for (size_t i = 0; i < nparts; i++) {
        char part[24];
        snprintf(part, sizeof part, "%ld", parts[i]);
        if (i) strncat(out, ".", sizeof(out) - strlen(out) - 1);
        strncat(out, part, sizeof(out) - strlen(out) - 1);
    }
    return true;
}

/* -------------------------------------------------------- string reading --- */

/** Strict UTF-8 validation (the fallback path in readString). */
static bool is_valid_utf8(const uint8_t *b, size_t len) {
    size_t i = 0;
    while (i < len) {
        if (b[i] < 0x80) { i++; continue; }
        size_t extra;
        if ((b[i] & 0xe0) == 0xc0) extra = 1;
        else if ((b[i] & 0xf0) == 0xe0) extra = 2;
        else if ((b[i] & 0xf8) == 0xf0) extra = 3;
        else return false;
        if (i + extra >= len) return false; /* truncated sequence */
        for (size_t j = 1; j <= extra; j++) {
            if ((b[i + j] & 0xc0) != 0x80) return false;
        }
        i += extra + 1;
    }
    return true;
}

/**
 * Decode a node's value as text: the string tags directly, else valid UTF-8,
 * else a colon-separated hex dump (the TS reference's last resort).
 */
static void read_string(const asn1_node *node, char out[512]) {
    long tag = node->tag_number;
    bool is_text_tag = tag == 12 || tag == 19 || tag == 22 || tag == 30 || tag == 36;
    size_t n = node->value_len < 511 ? node->value_len : 511;
    if ((is_text_tag || is_valid_utf8(node->value, node->value_len)) && n > 0) {
        memcpy(out, node->value, n);
        out[n] = 0;
        return;
    }
    if (n == 0) { out[0] = 0; return; }
    /* hex fallback */
    size_t pos = 0;
    for (size_t i = 0; i < node->value_len && pos + 3 < 512; i++) {
        if (i) out[pos++] = ':';
        pos += (size_t)snprintf(out + pos, 3, "%02X", node->value[i]);
    }
    out[pos] = 0;
}

/* ------------------------------------------------------------------ RDN ---- */

typedef struct {
    char type[64];  /* short name like 'CN', 'O', ... */
    char value[512];
} rdn_attribute;

typedef struct {
    rdn_attribute *items;
    size_t count;
} rdn_list;

static bool parse_rdn(const asn1_node *node, rdn_list *out) {
    for (size_t i = 0; i < node->child_count; i++) {          /* RDN sequences */
        const asn1_node *rdn_set = node->children[i];
        for (size_t j = 0; j < rdn_set->child_count; j++) {   /* AttributeTypeAndValue */
            const asn1_node *attr_seq = rdn_set->children[j];
            if (attr_seq->child_count < 2) continue;
            char oid[128];
            if (!decode_oid(attr_seq->children[0]->value, attr_seq->children[0]->value_len, oid)) continue;
            rdn_attribute attr;
            snprintf(attr.type, sizeof attr.type, "%s", oid_name(oid));
            read_string(attr_seq->children[1], attr.value);
            rdn_attribute *grown = realloc(out->items, (out->count + 1) * sizeof *grown);
            if (!grown) return false;
            out->items = grown;
            out->items[out->count++] = attr;
        }
    }
    return true;
}

/** "CN=example.com, O=CosmoLabs" — the TS formatDN join. */
static void format_dn(const rdn_list *attrs, char *out, size_t out_len) {
    size_t pos = 0;
    out[0] = 0;
    for (size_t i = 0; i < attrs->count && pos + 1 < out_len; i++) {
        int wrote = snprintf(out + pos, out_len - pos, "%s%s=%s", i ? ", " : "",
                             attrs->items[i].type, attrs->items[i].value);
        if (wrote < 0) break;
        if ((size_t)wrote >= out_len - pos) { pos = out_len - 1; break; }
        pos += (size_t)wrote;
    }
}

/* ----------------------------------------------------------------- time ---- */

static bool digits_only(const char *s, size_t n) {
    for (size_t i = 0; i < n; i++) {
        if (!isdigit((unsigned char)s[i])) return false;
    }
    return true;
}

/** UTCTime (tag 23) 'YYMMDDHHMMSSZ' / GeneralizedTime (24) 'YYYYMMDDHHMMSSZ'. */
static bool parse_time(const asn1_node *node, time_t *out) {
    char str[64];
    read_string(node, str);
    struct tm tm = {0};
    int year;
    if (node->tag_number == 23) {
        if (strlen(str) != 13 || str[12] != 'Z' || !digits_only(str, 12)) return false;
        year = (str[0] - '0') * 10 + (str[1] - '0');
        year += year >= 50 ? 1900 : 2000;
        tm.tm_mon = (str[2] - '0') * 10 + (str[3] - '0') - 1;
        tm.tm_mday = (str[4] - '0') * 10 + (str[5] - '0');
        tm.tm_hour = (str[6] - '0') * 10 + (str[7] - '0');
        tm.tm_min = (str[8] - '0') * 10 + (str[9] - '0');
        tm.tm_sec = (str[10] - '0') * 10 + (str[11] - '0');
    } else if (node->tag_number == 24) {
        if (strlen(str) != 15 || str[14] != 'Z' || !digits_only(str, 14)) return false;
        year = (str[0] - '0') * 1000 + (str[1] - '0') * 100 + (str[2] - '0') * 10 + (str[3] - '0');
        tm.tm_mon = (str[4] - '0') * 10 + (str[5] - '0') - 1;
        tm.tm_mday = (str[6] - '0') * 10 + (str[7] - '0');
        tm.tm_hour = (str[8] - '0') * 10 + (str[9] - '0');
        tm.tm_min = (str[10] - '0') * 10 + (str[11] - '0');
        tm.tm_sec = (str[12] - '0') * 10 + (str[13] - '0');
    } else {
        return false;
    }
    tm.tm_year = year - 1900;
    *out = timegm(&tm);
    return true;
}

/* ------------------------------------------------------------ extensions --- */

typedef struct {
    int basic_constraints_ca; /* -1 = null (absent) */
    str_list key_usage;       /* NULL items = null */
    str_list ext_key_usage;
    str_list san_dns;
    str_list san_ip;
    str_list san_email;
} extensions;

static void parse_bit_string(const asn1_node *node, str_list *out) {
    if (node->value_len < 2) return;
    unsigned unused_bits = node->value[0];
    const uint8_t *octets = node->value + 1;
    size_t octet_count = node->value_len - 1;

    for (size_t i = 0; i < sizeof KEY_USAGE_BITS / sizeof KEY_USAGE_BITS[0]; i++) {
        size_t octet_idx = i / 8;
        int bit_idx = 7 - (int)(i % 8);
        if (octet_idx < octet_count && (octets[octet_idx] & (1 << bit_idx)) != 0) {
            str_list_push(out, KEY_USAGE_BITS[i]);
        }
    }
    /* Mask out unused bits. */
    if (unused_bits > 0 && out->count > 0) {
        size_t total_bits = octet_count * 8 - unused_bits;
        while (out->count > total_bits) {
            free(out->items[out->count - 1]);
            out->count--;
        }
    }
}

static void parse_extensions(const asn1_node *const *nodes, size_t count, extensions *ext) {
    for (size_t n = 0; n < count; n++) {
        const asn1_node *ext_node = nodes[n];
        if (ext_node->child_count < 2) continue;
        char oid[128];
        if (!decode_oid(ext_node->children[0]->value, ext_node->children[0]->value_len, oid)) continue;

        /* Skip the critical boolean if present. */
        size_t value_idx = 1;
        if (ext_node->child_count >= 3 && ext_node->children[1]->tag_number == 1 &&
            ext_node->children[1]->klass == ASN1_UNIVERSAL) {
            value_idx = 2;
        }
        const asn1_node *value_node = ext_node->children[value_idx];
        const uint8_t *octet_content = value_node->value;
        size_t content_len = value_node->value_len;
        if (content_len == 0) continue;

        /* The value is an OCTET STRING wrapping the actual DER. */
        der_reader inner = {octet_content, content_len, 0};
        const char *err = NULL;
        asn1_node *content = der_read_node(&inner, &err);
        if (!content) continue;

        if (strcmp(oid, "2.5.29.19") == 0) {
            /* Basic Constraints */
            ext->basic_constraints_ca = 0;
            if (content->child_count > 0) {
                const asn1_node *bool_val = content->children[0];
                if (bool_val->tag_number == 1 && bool_val->value_len == 1 && bool_val->value[0] == 0xff) {
                    ext->basic_constraints_ca = 1;
                }
            }
        } else if (strcmp(oid, "2.5.29.15") == 0) {
            parse_bit_string(content, &ext->key_usage); /* Key Usage — BIT STRING */
        } else if (strcmp(oid, "2.5.29.37") == 0) {
            /* Extended Key Usage */
            for (size_t i = 0; i < content->child_count; i++) {
                char purpose[128];
                if (decode_oid(content->children[i]->value, content->children[i]->value_len, purpose)) {
                    str_list_push(&ext->ext_key_usage, oid_name(purpose));
                }
            }
        } else if (strcmp(oid, "2.5.29.17") == 0) {
            /* Subject Alternative Name */
            for (size_t i = 0; i < content->child_count; i++) {
                const asn1_node *child = content->children[i];
                if (child->klass != ASN1_CONTEXT) continue;
                if (child->tag_number == 2) {
                    char dns[512];
                    read_string(child, dns);
                    str_list_push(&ext->san_dns, dns);
                } else if (child->tag_number == 7) {
                    char ip[64];
                    if (child->value_len == 4) {
                        snprintf(ip, sizeof ip, "%u.%u.%u.%u",
                                 child->value[0], child->value[1], child->value[2], child->value[3]);
                        str_list_push(&ext->san_ip, ip);
                    } else if (child->value_len == 16) {
                        /* IPv6 — standard hex representation */
                        char parts[8][5];
                        for (int g = 0; g < 8; g++) {
                            snprintf(parts[g], sizeof parts[g], "%x",
                                     (child->value[g * 2] << 8) | child->value[g * 2 + 1]);
                        }
                        snprintf(ip, sizeof ip, "%s:%s:%s:%s:%s:%s:%s:%s",
                                 parts[0], parts[1], parts[2], parts[3],
                                 parts[4], parts[5], parts[6], parts[7]);
                        str_list_push(&ext->san_ip, ip);
                    }
                } else if (child->tag_number == 1) {
                    char email[512];
                    read_string(child, email);
                    str_list_push(&ext->san_email, email);
                }
            }
        }
        asn1_free(content);
    }
}

/* ------------------------------------------------------------ public type --- */

typedef struct {
    rdn_list subject;
    char *subject_dn;
    rdn_list issuer;
    char *issuer_dn;
    time_t not_before;
    time_t not_after;
    char *serial_number;      /* colon-separated hex, uppercase */
    char *signature_algorithm;
    char *key_algorithm;
    long key_size;            /* bits */
    int basic_constraints_ca; /* -1 = null */
    str_list key_usage;
    str_list ext_key_usage;
    str_list san_dns;
    str_list san_ip;
    str_list san_email;
    long version;             /* 0=v1, 1=v2, 2=v3 */
    uint8_t *raw_der;         /* for fingerprinting */
    size_t raw_der_len;
} certificate_info;

void certificate_info_free(certificate_info *cert) {
    free(cert->subject.items);
    free(cert->subject_dn);
    free(cert->issuer.items);
    free(cert->issuer_dn);
    free(cert->serial_number);
    free(cert->signature_algorithm);
    free(cert->key_algorithm);
    str_list_free(&cert->key_usage);
    str_list_free(&cert->ext_key_usage);
    str_list_free(&cert->san_dns);
    str_list_free(&cert->san_ip);
    str_list_free(&cert->san_email);
    free(cert->raw_der);
    memset(cert, 0, sizeof *cert);
}

/* ------------------------------------------------------------ PEM / base64 --- */

static int b64_value(char c) {
    if (c >= 'A' && c <= 'Z') return c - 'A';
    if (c >= 'a' && c <= 'z') return c - 'a' + 26;
    if (c >= '0' && c <= '9') return c - '0' + 52;
    if (c == '+') return 62;
    if (c == '/') return 63;
    return -1;
}

/** Standard base64 (padding tolerated) -> bytes. Returns false on bad input. */
static bool base64_decode(const char *src, size_t src_len, uint8_t **out, size_t *out_len) {
    uint8_t *buf = malloc(src_len ? src_len / 4 * 3 + 3 : 1);
    if (!buf) return false;
    size_t n = 0;
    int quad[4], quad_len = 0;
    for (size_t i = 0; i < src_len; i++) {
        if (src[i] == '=') break; /* padding — remainder handled below */
        int v = b64_value(src[i]);
        if (v < 0) { free(buf); return false; }
        quad[quad_len++] = v;
        if (quad_len == 4) {
            buf[n++] = (uint8_t)((quad[0] << 2) | (quad[1] >> 4));
            buf[n++] = (uint8_t)(((quad[1] & 0x0f) << 4) | (quad[2] >> 2));
            buf[n++] = (uint8_t)(((quad[2] & 0x03) << 6) | quad[3]);
            quad_len = 0;
        }
    }
    if (quad_len == 1) { free(buf); return false; } /* impossible remainder */
    if (quad_len >= 2) {
        buf[n++] = (uint8_t)((quad[0] << 2) | (quad[1] >> 4));
        if (quad_len == 3) buf[n++] = (uint8_t)(((quad[1] & 0x0f) << 4) | (quad[2] >> 2));
    }
    *out = buf;
    *out_len = n;
    return true;
}

/**
 * Pull every `-----BEGIN CERTIFICATE-----` block's base64 payload out of a PEM
 * string. Returns the number of blocks found (0 = none).
 */
static size_t pem_extract_blocks(const char *pem, uint8_t ***blocks_out, size_t **lens_out) {
    uint8_t **blocks = NULL;
    size_t *lens = NULL;
    size_t count = 0;
    const char *cursor = pem;
    static const char *BEGIN = "-----BEGIN CERTIFICATE-----";
    static const char *END = "-----END CERTIFICATE-----";
    while ((cursor = strstr(cursor, BEGIN)) != NULL) {
        const char *body = cursor + strlen(BEGIN);
        const char *end = strstr(body, END);
        if (!end) break;
        /* strip whitespace within the base64 body */
        char *clean = malloc((size_t)(end - body) + 1);
        if (!clean) break;
        size_t n = 0;
        for (const char *p = body; p < end; p++) {
            if (!isspace((unsigned char)*p)) clean[n++] = *p;
        }
        clean[n] = 0;
        uint8_t *der = NULL;
        size_t der_len = 0;
        bool ok = n > 0 && base64_decode(clean, n, &der, &der_len);
        free(clean);
        if (ok) {
            uint8_t **bg = realloc(blocks, (count + 1) * sizeof *bg);
            size_t *lg = realloc(lens, (count + 1) * sizeof *lg);
            if (bg && lg) {
                blocks = bg;
                lens = lg;
                blocks[count] = der;
                lens[count] = der_len;
                count++;
            } else {
                free(der);
                free(bg ? bg : blocks);
            }
        }
        cursor = end + strlen(END);
    }
    *blocks_out = blocks;
    *lens_out = lens;
    return count;
}

/* ------------------------------------------------------------ the decoder --- */

static const char *decode_certificate_der(const uint8_t *der, size_t der_len, certificate_info *cert) {
    memset(cert, 0, sizeof *cert);
    cert->basic_constraints_ca = -1;
    const char *err = NULL;

    /* Parse the outer SEQUENCE. */
    der_reader parser = {der, der_len, 0};
    asn1_node *cert_seq = der_read_node(&parser, &err);
    if (!cert_seq) return err ? err : "Invalid certificate structure";
    if (cert_seq->child_count < 3) {
        asn1_free(cert_seq);
        return "Invalid certificate structure: expected TBSCertificate, signatureAlgorithm, signatureValue";
    }

    const asn1_node *tbs = cert_seq->children[0];
    const asn1_node *sig_alg_node = cert_seq->children[1];
    char sig_alg_oid[128] = "";
    if (sig_alg_node->child_count >= 1) {
        decode_oid(sig_alg_node->children[0]->value, sig_alg_node->children[0]->value_len, sig_alg_oid);
    }

    if (tbs->child_count < 7) {
        asn1_free(cert_seq);
        return "Invalid TBSCertificate structure";
    }

    size_t idx = 0;

    /* Version (explicit context [0]) */
    long version = 0; /* default v1 */
    if (tbs->children[idx]->klass == ASN1_CONTEXT && tbs->children[idx]->tag_number == 0) {
        if (tbs->children[idx]->child_count > 0) {
            const asn1_node *version_node = tbs->children[idx]->children[0];
            if (version_node->value_len == 1) version = version_node->value[0];
        }
        idx++;
    }
    cert->version = version;

    /* Serial Number */
    const asn1_node *serial_node = tbs->children[idx++];
    idx++; /* skip the inner signature algorithm */

    /* Issuer */
    rdn_list issuer = {0};
    parse_rdn(tbs->children[idx++], &issuer);
    cert->issuer = issuer;

    /* Validity */
    const asn1_node *validity_node = tbs->children[idx++];
    if (validity_node->child_count >= 2) {
        parse_time(validity_node->children[0], &cert->not_before);
        parse_time(validity_node->children[1], &cert->not_after);
    }

    /* Subject */
    rdn_list subject = {0};
    parse_rdn(tbs->children[idx++], &subject);
    cert->subject = subject;

    /* SubjectPublicKeyInfo */
    const asn1_node *spki = tbs->children[idx++];
    char key_alg_oid[128] = "";
    if (spki->child_count >= 2) {
        const asn1_node *alg_seq = spki->children[0];
        const uint8_t *key_bits = spki->children[1]->value;
        size_t key_bits_len = spki->children[1]->value_len;

        if (alg_seq->child_count >= 1 &&
            decode_oid(alg_seq->children[0]->value, alg_seq->children[0]->value_len, key_alg_oid)) {
            /* Key size estimation */
            if (strcmp(key_alg_oid, "1.2.840.113549.1.1.1") == 0) {
                /* RSA: the BIT STRING starts with an unused-bits byte (0x00),
                 * then a DER SEQUENCE of { modulus INTEGER, exponent INTEGER }. */
                if (key_bits_len > 1) {
                    der_reader rsa_inner = {key_bits + 1, key_bits_len - 1, 0};
                    asn1_node *rsa_seq = der_read_node(&rsa_inner, &err);
                    if (rsa_seq) {
                        if (rsa_seq->child_count > 0) {
                            const uint8_t *mod = rsa_seq->children[0]->value;
                            size_t mod_len = rsa_seq->children[0]->value_len;
                            /* First byte may be 0x00 padding for positive sign. */
                            if (mod_len > 0 && mod[0] == 0x00) mod_len--;
                            cert->key_size = (long)mod_len * 8;
                        }
                        asn1_free(rsa_seq);
                    }
                }
            } else if (strcmp(key_alg_oid, "1.2.840.10045.2.1") == 0) {
                /* ECDSA: name the curve, size = uncompressed point minus 0x04. */
                char curve[64] = "";
                char curve_oid[128];
                if (alg_seq->child_count >= 2 &&
                    decode_oid(alg_seq->children[1]->value, alg_seq->children[1]->value_len, curve_oid)) {
                    const char *name = oid_table_lookup(EC_CURVE_NAMES,
                                                        sizeof EC_CURVE_NAMES / sizeof EC_CURVE_NAMES[0],
                                                        curve_oid);
                    if (name) snprintf(curve, sizeof curve, "%s", name);
                }
                char label[128];
                if (curve[0]) {
                    snprintf(label, sizeof label, "%s (%s)", oid_name(key_alg_oid), curve);
                } else {
                    snprintf(label, sizeof label, "%s", oid_name(key_alg_oid));
                }
                cert->key_algorithm = strdup(label);
                if (key_bits_len > 1) cert->key_size = (long)(key_bits_len - 2) * 8;
            } else {
                cert->key_size = key_bits_len > 1 ? (long)(key_bits_len - 1) * 8 : 0;
            }
        }
    }
    if (!cert->key_algorithm) {
        cert->key_algorithm = strdup(key_alg_oid[0] ? oid_name(key_alg_oid) : "unknown");
    }

    /* Extensions — look for context [3] after subjectPublicKeyInfo */
    while (idx < tbs->child_count) {
        const asn1_node *child = tbs->children[idx++];
        if (child->klass == ASN1_CONTEXT && child->tag_number == 3 && child->child_count > 0) {
            const asn1_node *outer_seq = child->children[0];
            if (outer_seq->child_count > 0) {
                extensions ext = {-1, {0}, {0}, {0}, {0}, {0}};
                parse_extensions(outer_seq->children, outer_seq->child_count, &ext);
                cert->basic_constraints_ca = ext.basic_constraints_ca;
                cert->key_usage = ext.key_usage;
                cert->ext_key_usage = ext.ext_key_usage;
                cert->san_dns = ext.san_dns;
                cert->san_ip = ext.san_ip;
                cert->san_email = ext.san_email;
            }
        }
    }

    /* Serial number as hex */
    if (serial_node->value_len > 0) {
        const uint8_t *sb = serial_node->value;
        size_t sb_len = serial_node->value_len;
        if (sb[0] == 0x00 && sb_len > 1) { sb++; sb_len--; } /* sign padding */
        char *hex = malloc(sb_len * 3);
        size_t pos = 0;
        for (size_t i = 0; i < sb_len; i++) {
            pos += (size_t)snprintf(hex + pos, 4, "%02X:", sb[i]);
        }
        if (pos) hex[pos - 1] = 0; /* trailing colon */
        cert->serial_number = hex;
    } else {
        cert->serial_number = strdup("");
    }

    cert->signature_algorithm = strdup(oid_name(sig_alg_oid));
    cert->subject_dn = malloc(1024);
    cert->issuer_dn = malloc(1024);
    format_dn(&cert->subject, cert->subject_dn, 1024);
    format_dn(&cert->issuer, cert->issuer_dn, 1024);
    cert->raw_der = malloc(der_len ? der_len : 1);
    memcpy(cert->raw_der, der, der_len);
    cert->raw_der_len = der_len;

    asn1_free(cert_seq);
    return NULL;
}

const char *decode_certificate(const char *pem, certificate_info *cert) {
    if (!pem || !*pem || strspn(pem, " \t\r\n") == strlen(pem)) {
        return "Empty input — paste a PEM certificate";
    }
    uint8_t **blocks = NULL;
    size_t *lens = NULL;
    size_t count = pem_extract_blocks(pem, &blocks, &lens);
    if (count == 0) {
        return "No PEM certificate block found — expected -----BEGIN CERTIFICATE-----";
    }
    const char *err = decode_certificate_der(blocks[0], lens[0], cert);
    for (size_t i = 0; i < count; i++) free(blocks[i]);
    free(blocks);
    free(lens);
    return err;
}

bool is_expired(const certificate_info *cert) { return cert->not_after < time(NULL); }

long days_until_expiry(const certificate_info *cert) {
    double diff_s = difftime(cert->not_after, time(NULL));
    return (long)ceil(diff_s / (60.0 * 60.0 * 24.0));
}

/** Extract all PEM certificate blocks and decode each. count==0 -> error. */
const char *decode_certificate_chain(const char *pem, certificate_info **chain_out, size_t *count_out) {
    if (!pem || !*pem || strspn(pem, " \t\r\n") == strlen(pem)) {
        return "Empty input — paste PEM certificate(s)";
    }
    uint8_t **blocks = NULL;
    size_t *lens = NULL;
    size_t count = pem_extract_blocks(pem, &blocks, &lens);
    if (count == 0) {
        return "No PEM certificate block found — expected -----BEGIN CERTIFICATE-----";
    }
    certificate_info *chain = calloc(count, sizeof *chain);
    if (!chain) {
        for (size_t i = 0; i < count; i++) free(blocks[i]);
        free(blocks);
        free(lens);
        return "out of memory";
    }
    const char *first_err = NULL;
    for (size_t i = 0; i < count; i++) {
        const char *err = decode_certificate_der(blocks[i], lens[i], &chain[i]);
        if (err && !first_err) first_err = err;
    }
    for (size_t i = 0; i < count; i++) free(blocks[i]);
    free(blocks);
    free(lens);
    if (first_err) { /* free what we can and report */
        for (size_t i = 0; i < count; i++) certificate_info_free(&chain[i]);
        free(chain);
        return first_err;
    }
    *chain_out = chain;
    *count_out = count;
    return NULL;
}

/* ------------------------------------------------------------- demo main --- */

int main(void) {
    /* A real-looking self-signed RSA-2048 certificate PEM (truncated body for
     * the demo — decode_certificate parses whatever is well-formed). */
    const char *pem = "-----BEGIN CERTIFICATE-----\n"
                      "MIIB\n"
                      "-----END CERTIFICATE-----\n";
    certificate_info cert;
    const char *err = decode_certificate(pem, &cert);
    if (err) {
        printf("decode error (expected for the stub body): %s\n", err);
        return 0;
    }
    printf("subject: %s\n", cert.subject_dn);
    printf("issuer:  %s\n", cert.issuer_dn);
    printf("serial:  %s\n", cert.serial_number);
    certificate_info_free(&cert);
    return 0;
}

Also available in 8 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →