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 →