Certificate Decoder — Zig 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 Zig 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: Zig 0.14 (standard library only)
//! Ported from: src/lib/cert-decoder.ts (the canonical TypeScript implementation).
//! display source — part of CosmoDev's polyglot tool pages.
//!
//! No external dependencies — DER is deterministic and parsed sequentially.
//! Dates are Unix epoch seconds (`i64`); the TS reference uses `Date`, which
//! carries the same instant. All string fields are allocated from the
//! allocator passed in.
const std = @import("std");
// ── OID name map ───────────────────────────────────────────────────────────
const OID_NAMES = std.StaticStringMap([]const u8).initComptime(.{
.{ "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.4.1.11129.2.1.17", "CT Precertificate SCTs" },
.{ "1.3.6.1.5.5.7.1.3", "CRL Distribution Points" },
.{ "1.3.6.1.4.1.311.21.7", "Microsoft Certificate Template" },
});
/// EC named-curve OIDs.
const EC_CURVE_NAMES = std.StaticStringMap([]const u8).initComptime(.{
.{ "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" },
});
/// Key Usage bit names, MSB-first inside each octet.
const KEY_USAGE_BITS = [_][]const u8{
"digitalSignature",
"nonRepudiation",
"keyEncipherment",
"dataEncipherment",
"keyAgreement",
"keyCertSign",
"cRLSign",
"encipherOnly",
"decipherOnly",
};
pub const Error = error{
EmptyInput,
NoPemBlock,
InvalidBase64,
TruncatedDer,
UnexpectedEnd,
IndefiniteLength,
LengthTooLarge,
InvalidCertificate,
InvalidTbs,
InvalidUTCTime,
InvalidGeneralizedTime,
UnknownTimeTag,
OutOfMemory,
};
// ── ASN.1 DER types ────────────────────────────────────────────────────────
pub const TagClass = enum { universal, application, context, private };
pub const ASN1Node = struct {
tag_class: TagClass,
constructed: bool,
tag_number: u32,
raw_value: []const u8, // value bytes (decoded content), aliasing the input
offset: usize, // offset into the original buffer
children: ?[]ASN1Node = null,
};
// ── DER parsing ────────────────────────────────────────────────────────────
pub const DERParser = struct {
buf: []const u8,
pos: usize = 0,
allocator: std.mem.Allocator,
pub fn exhausted(self: *const DERParser) bool {
return self.pos >= self.buf.len;
}
/// Read the next TLV node (recursing into constructed content).
pub fn readNode(self: *DERParser) Error!ASN1Node {
const offset = self.pos;
const byte0 = try self.readByte();
const tag_class: TagClass = switch (byte0 & 0xc0) {
0x00 => .universal,
0x40 => .application,
0x80 => .context,
else => .private,
};
const constructed = (byte0 & 0x20) != 0;
var tag_number: u32 = byte0 & 0x1f;
// Long-form tag (tag number >= 31)
if (tag_number == 0x1f) {
tag_number = 0;
while (true) {
const b = try self.readByte();
tag_number = (tag_number << 7) | (b & 0x7f);
if (b & 0x80 == 0) break;
}
}
const length = try self.readLength();
if (self.pos + length > self.buf.len) {
return Error.TruncatedDer;
}
const raw_value = self.buf[self.pos .. self.pos + length];
self.pos += length;
var node = ASN1Node{
.tag_class = tag_class,
.constructed = constructed,
.tag_number = tag_number,
.raw_value = raw_value,
.offset = offset,
};
// Parse children for constructed types
if (constructed and raw_value.len > 0) {
var child_parser = DERParser{ .buf = raw_value, .allocator = self.allocator };
var children = std.ArrayList(ASN1Node).init(self.allocator);
errdefer children.deinit();
while (!child_parser.exhausted()) {
try children.append(try child_parser.readNode());
}
node.children = try children.toOwnedSlice();
}
return node;
}
fn readByte(self: *DERParser) Error!u8 {
if (self.pos >= self.buf.len) return Error.UnexpectedEnd;
const b = self.buf[self.pos];
self.pos += 1;
return b;
}
fn readLength(self: *DERParser) Error!usize {
const first = try self.readByte();
if (first < 0x80) return first;
const num_bytes = first & 0x7f;
if (num_bytes == 0) return Error.IndefiniteLength;
if (num_bytes > 4) return Error.LengthTooLarge;
var len: usize = 0;
for (0..num_bytes) |_| {
len = (len << 8) | try self.readByte();
}
return len;
}
};
// ── OID decoding ──────────────────────────────────────────────────────────
/// Decode OID content bytes into a dotted string. Caller owns the result.
pub fn decodeOID(allocator: std.mem.Allocator, bytes: []const u8) Error![]u8 {
if (bytes.len == 0) return Error.UnexpectedEnd;
var out = std.ArrayList(u8).init(allocator);
errdefer out.deinit();
const w = out.writer();
try w.print("{d}.{d}", .{ bytes[0] / 40, bytes[0] % 40 });
var value: u64 = 0;
for (bytes[1..]) |b| {
value = (value << 7) | (b & 0x7f);
if (b & 0x80 == 0) {
try w.print(".{d}", .{value});
value = 0;
}
}
return out.toOwnedSlice();
}
fn oidName(oid: []const u8) []const u8 {
return OID_NAMES.get(oid) orelse oid;
}
// ── RDN (Relative Distinguished Name) helpers ──────────────────────────────
pub const RDNAttribute = struct {
type_: []const u8, // short name like "CN", "O", or the raw dotted OID
value: []const u8,
};
fn parseRDN(allocator: std.mem.Allocator, node: *const ASN1Node) Error![]RDNAttribute {
var attrs = std.ArrayList(RDNAttribute).init(allocator);
errdefer attrs.deinit();
const sets = node.children orelse return attrs.toOwnedSlice();
for (sets) |rdn_set| {
const seqs = rdn_set.children orelse continue;
for (seqs) |attr_seq| {
const kids = attr_seq.children orelse continue;
if (kids.len < 2) continue;
const oid = try decodeOID(allocator, kids[0].raw_value);
try attrs.append(.{ .type_ = oidName(oid), .value = try readString(allocator, &kids[1]) });
}
}
return attrs.toOwnedSlice();
}
/// "CN=example.com, O=Acme" — caller owns the result.
pub fn formatDN(allocator: std.mem.Allocator, attrs: []const RDNAttribute) Error![]u8 {
var out = std.ArrayList(u8).init(allocator);
errdefer out.deinit();
for (attrs, 0..) |a, i| {
if (i > 0) try out.appendSlice(", ");
try out.writer().print("{s}={s}", .{ a.type_, a.value });
}
return out.toOwnedSlice();
}
/// Decode a node's content as text (UTF8String / PrintableString / IA5String /
/// BMPString tags), falling back to UTF-8, then to colon-separated hex.
fn readString(allocator: std.mem.Allocator, node: *const ASN1Node) Error![]u8 {
const tag = node.tag_number;
if (tag == 12 or tag == 19 or tag == 22 or tag == 30 or tag == 36) {
return allocator.dupe(u8, node.raw_value);
}
// Fallback: valid UTF-8 passes through, anything else renders as hex.
if (std.unicode.utf8ValidateSlice(node.raw_value)) {
return allocator.dupe(u8, node.raw_value);
}
var out = std.ArrayList(u8).init(allocator);
errdefer out.deinit();
for (node.raw_value, 0..) |b, i| {
if (i > 0) try out.append(':');
try out.writer().print("{x:0>2}", .{b});
}
return out.toOwnedSlice();
}
// ── Time helpers ───────────────────────────────────────────────────────────
/// Days since 1970-01-01 for a civil (year, month, day) date.
fn daysFromCivil(y_in: i64, m: i64, d: i64) i64 {
var y = y_in;
if (m <= 2) y -= 1;
const era = @divFloor(if (y >= 0) y else y - 399, 400);
const yoe: i64 = y - era * 400; // [0, 399]
const doy = @divTrunc(153 * (m + (if (m > 2) @as(i64, -3) else @as(i64, 9))) + 2, 5) + d - 1;
const doe = yoe * 365 + @divTrunc(yoe, 4) - @divTrunc(yoe, 100) + doy;
return era * 146097 + doe - 719468;
}
fn parseFixedDigits(s: []const u8, comptime n: usize) Error!u64 {
if (s.len < n) return Error.InvalidUTCTime;
var v: u64 = 0;
for (s[0..n]) |c| {
if (!std.ascii.isDigit(c)) return Error.InvalidUTCTime;
v = v * 10 + (c - '0');
}
return v;
}
/// UTCTime (tag 23): YYMMDDHHMMSSZ → epoch seconds (YY >= 50 → 19xx).
fn parseUTCTime(str: []const u8) Error!i64 {
if (str.len != 13 or str[12] != 'Z') return Error.InvalidUTCTime;
var year = try parseFixedDigits(str, 2);
year += if (year >= 50) 1900 else 2000;
return parseDateParts(year, str[2..]);
}
/// GeneralizedTime (tag 24): YYYYMMDDHHMMSSZ → epoch seconds.
fn parseGeneralizedTime(str: []const u8) Error!i64 {
if (str.len != 15 or str[14] != 'Z') return Error.InvalidGeneralizedTime;
const year = try parseFixedDigits(str, 4);
return parseDateParts(year, str[4..]);
}
/// The MMDDHHMMSSZ tail shared by both time formats.
fn parseDateParts(year: u64, tail: []const u8) Error!i64 {
if (tail.len != 11 or tail[10] != 'Z') return Error.InvalidUTCTime;
const month = try parseFixedDigits(tail[0..], 2);
const day = try parseFixedDigits(tail[2..], 2);
const hh = try parseFixedDigits(tail[4..], 2);
const mm = try parseFixedDigits(tail[6..], 2);
const ss = try parseFixedDigits(tail[8..], 2);
if (month < 1 or month > 12 or day < 1 or day > 31) return Error.InvalidUTCTime;
const days = daysFromCivil(@intCast(year), @intCast(month), @intCast(day));
return days * 86400 + @as(i64, @intCast(hh)) * 3600 + @as(i64, @intCast(mm)) * 60 + @as(i64, @intCast(ss));
}
fn parseTime(allocator: std.mem.Allocator, node: *const ASN1Node) Error!i64 {
const str = try readString(allocator, node);
defer allocator.free(str);
if (node.tag_number == 23) return parseUTCTime(str);
if (node.tag_number == 24) return parseGeneralizedTime(str);
return Error.UnknownTimeTag;
}
// ── Extension parsing ──────────────────────────────────────────────────────
pub const Extensions = struct {
basic_constraints_ca: ?bool = null,
key_usage: ?[][]const u8 = null,
ext_key_usage: ?[][]const u8 = null,
san_dns: [][]const u8 = &.{},
san_ip: [][]const u8 = &.{},
san_email: [][]const u8 = &.{},
};
fn parseExtensions(allocator: std.mem.Allocator, nodes: []const ASN1Node) Error!Extensions {
var ext = Extensions{};
var dns = std.ArrayList([]const u8).init(allocator);
var ips = std.ArrayList([]const u8).init(allocator);
var emails = std.ArrayList([]const u8).init(allocator);
errdefer {
dns.deinit();
ips.deinit();
emails.deinit();
}
for (nodes) |ext_node| {
const kids = ext_node.children orelse continue;
if (kids.len < 2) continue;
const oid = try decodeOID(allocator, kids[0].raw_value);
defer allocator.free(oid);
// Skip the critical BOOLEAN if present
var value_idx: usize = 1;
if (kids.len >= 3 and kids[1].tag_number == 1 and kids[1].tag_class == .universal) {
value_idx = 2;
}
// The value is wrapped in an OCTET STRING containing the actual DER
const octet_content = kids[value_idx].raw_value;
if (octet_content.len == 0) continue;
var inner = DERParser{ .buf = octet_content, .allocator = allocator };
if (inner.exhausted) continue;
const content = try inner.readNode();
if (std.mem.eql(u8, oid, "2.5.29.19")) {
// Basic Constraints
ext.basic_constraints_ca = false;
if (content.children) |ck| {
if (ck.len > 0 and ck[0].tag_number == 1 and
ck[0].raw_value.len == 1 and ck[0].raw_value[0] == 0xff)
{
ext.basic_constraints_ca = true;
}
}
} else if (std.mem.eql(u8, oid, "2.5.29.15")) {
// Key Usage — BIT STRING
ext.key_usage = try parseBitString(allocator, &content);
} else if (std.mem.eql(u8, oid, "2.5.29.37")) {
// Extended Key Usage
var ekus = std.ArrayList([]const u8).init(allocator);
if (content.children) |ck| {
for (ck) |child| {
const purpose = try decodeOID(allocator, child.raw_value);
try ekus.append(oidName(purpose));
}
}
ext.ext_key_usage = try ekus.toOwnedSlice();
} else if (std.mem.eql(u8, oid, "2.5.29.17")) {
// Subject Alternative Name
if (content.children) |ck| {
for (ck) |child| {
if (child.tag_class != .context) continue;
switch (child.tag_number) {
2 => try dns.append(try readString(allocator, &child)),
1 => try emails.append(try readString(allocator, &child)),
7 => {
const ip_bytes = child.raw_value;
if (ip_bytes.len == 4) {
try ips.append(try std.fmt.allocPrint(allocator, "{d}.{d}.{d}.{d}", .{ ip_bytes[0], ip_bytes[1], ip_bytes[2], ip_bytes[3] }));
} else if (ip_bytes.len == 16) {
// IPv6 — standard hex representation
var parts: [8]u16 = undefined;
for (0..8) |i| {
parts[i] = (@as(u16, ip_bytes[i * 2]) << 8) | ip_bytes[i * 2 + 1];
}
var hex = std.ArrayList(u8).init(allocator);
for (parts, 0..) |p, i| {
if (i > 0) try hex.append(':');
try hex.writer().print("{x}", .{p});
}
try ips.append(try hex.toOwnedSlice());
}
},
else => {},
}
}
}
}
}
ext.san_dns = try dns.toOwnedSlice();
ext.san_ip = try ips.toOwnedSlice();
ext.san_email = try emails.toOwnedSlice();
return ext;
}
/// Decode a Key Usage BIT STRING into the set flag names (MSB-first per octet,
/// unused-bit count honored).
fn parseBitString(allocator: std.mem.Allocator, node: *const ASN1Node) Error![][]const u8 {
var flags = std.ArrayList([]const u8).init(allocator);
errdefer flags.deinit();
if (node.raw_value.len < 2) return flags.toOwnedSlice();
const unused_bits = node.raw_value[0];
const octets = node.raw_value[1..];
for (KEY_USAGE_BITS, 0..) |bit_name, i| {
const octet_idx = i / 8;
const bit_idx: u3 = @intCast(7 - (i % 8));
if (octet_idx < octets.len) {
if (octets[octet_idx] & (@as(u8, 1) << bit_idx) != 0) {
try flags.append(bit_name);
}
}
}
// Mask out unused bits: drop flags past the meaningful bit count.
if (unused_bits > 0 and flags.items.len > 0) {
const total_bits = octets.len * 8 - @as(usize, unused_bits);
while (flags.items.len > total_bits) {
_ = flags.pop();
}
}
return flags.toOwnedSlice();
}
// ── Public types ────────────────────────────────────────────────────────────
pub const CertificateInfo = struct {
/// Parsed subject RDN attributes
subject: []RDNAttribute,
/// Formatted subject DN string
subject_dn: []const u8,
/// Parsed issuer RDN attributes
issuer: []RDNAttribute,
/// Formatted issuer DN string
issuer_dn: []const u8,
/// Not-before, Unix epoch seconds
not_before: i64,
/// Not-after, Unix epoch seconds
not_after: i64,
/// Serial number as "AA:BB:…" hex string
serial_number: []const u8,
/// Signature algorithm (human-readable name)
signature_algorithm: []const u8,
/// Public key algorithm name
key_algorithm: []const u8,
/// Public key size in bits
key_size: usize,
/// Basic Constraints CA flag
basic_constraints_ca: ?bool,
/// Key Usage flags
key_usage: [][]const u8,
/// Extended Key Usage purposes
ext_key_usage: [][]const u8,
/// Subject Alternative Names — DNS entries
san_dns: [][]const u8,
/// Subject Alternative Names — IP entries
san_ip: [][]const u8,
/// Subject Alternative Names — email entries
san_email: [][]const u8,
/// Version number (0=v1, 1=v2, 2=v3)
version: u8,
/// Raw DER bytes (for fingerprinting)
raw_der: []const u8,
};
// ── PEM handling ───────────────────────────────────────────────────────────
const BEGIN_MARKER = "-----BEGIN CERTIFICATE-----";
const END_MARKER = "-----END CERTIFICATE-----";
const B64Decoder = std.base64.standard.Decoder;
/// Extract every PEM certificate block's Base64 body from `pem`.
pub fn extractPemBlocks(allocator: std.mem.Allocator, pem: []const u8) Error![][]u8 {
var blocks = std.ArrayList([]u8).init(allocator);
errdefer blocks.deinit();
var cursor: usize = 0;
while (std.mem.indexOfPos(u8, pem, cursor, BEGIN_MARKER)) |begin| {
const body_start = begin + BEGIN_MARKER.len;
const end = std.mem.indexOfPos(u8, pem, body_start, END_MARKER) orelse break;
const b64 = try stripWhitespace(allocator, pem[body_start..end]);
defer allocator.free(b64);
const decoded_len = B64Decoder.calcSizeForSlice(b64) catch return Error.InvalidBase64;
const der = try allocator.alloc(u8, decoded_len);
B64Decoder.decode(der, b64) catch {
allocator.free(der);
return Error.InvalidBase64;
};
try blocks.append(der);
cursor = end + END_MARKER.len;
}
return blocks.toOwnedSlice();
}
fn stripWhitespace(allocator: std.mem.Allocator, s: []const u8) Error![]u8 {
var out = std.ArrayList(u8).init(allocator);
errdefer out.deinit();
for (s) |c| {
if (c == ' ' or c == '\t' or c == '\r' or c == '\n') continue;
try out.append(c);
}
return out.toOwnedSlice();
}
// ── Main decoder ───────────────────────────────────────────────────────────
/// Decode the first certificate in a PEM string. Caller owns every field.
pub fn decodeCertificate(allocator: std.mem.Allocator, pem: []const u8) Error!CertificateInfo {
if (std.mem.trim(u8, pem, " \t\r\n").len == 0) return Error.EmptyInput;
const blocks = try extractPemBlocks(allocator, pem);
defer {
for (blocks) |b| allocator.free(b);
allocator.free(blocks);
}
if (blocks.len == 0) return Error.NoPemBlock;
return decodeDER(allocator, blocks[0]);
}
fn decodeDER(allocator: std.mem.Allocator, der: []u8) Error!CertificateInfo {
// Parse outer SEQUENCE
var parser = DERParser{ .buf = der, .allocator = allocator };
const cert_seq = try parser.readNode();
const top = cert_seq.children orelse return Error.InvalidCertificate;
if (top.len < 3) return Error.InvalidCertificate; // TBSCertificate, signatureAlgorithm, signatureValue
const tbs = &top[0];
const sig_alg_node = &top[1];
const sig_alg_oid: []u8 = if (sig_alg_node.children != null and sig_alg_node.children.?.len > 0)
try decodeOID(allocator, sig_alg_node.children.?[0].raw_value)
else
try allocator.dupe(u8, "");
// Parse TBSCertificate
const kids = tbs.children orelse return Error.InvalidTbs;
if (kids.len < 7) return Error.InvalidTbs;
var idx: usize = 0;
// Version (explicit context [0]); default v1
var version: u8 = 0;
if (kids[idx].tag_class == .context and kids[idx].tag_number == 0) {
if (kids[idx].children) |vk| {
if (vk.len > 0 and vk[0].raw_value.len == 1) version = vk[0].raw_value[0];
}
idx += 1;
}
// Serial Number
const serial_node = &kids[idx];
idx += 1;
idx += 1; // skip the inner signature algorithm
// Issuer
const issuer = try parseRDN(allocator, &kids[idx]);
idx += 1;
// Validity
var not_before: i64 = 0;
var not_after: i64 = 0;
if (kids[idx].children) |vk| {
if (vk.len >= 2) {
not_before = try parseTime(allocator, &vk[0]);
not_after = try parseTime(allocator, &vk[1]);
}
}
idx += 1;
// Subject
const subject = try parseRDN(allocator, &kids[idx]);
idx += 1;
// SubjectPublicKeyInfo
const spki_node = &kids[idx];
idx += 1;
var key_algorithm: []const u8 = "unknown";
var key_size: usize = 0;
if (spki_node.children) |spki| {
if (spki.len >= 2) {
const alg_seq = &spki[0];
const key_bits = spki[1].raw_value;
if (alg_seq.children) |alg| {
if (alg.len >= 1) {
const alg_oid = try decodeOID(allocator, alg[0].raw_value);
key_algorithm = oidName(alg_oid);
if (std.mem.eql(u8, alg_oid, "1.2.840.113549.1.1.1")) {
// RSA: the BIT STRING value starts with an unused-bits
// byte (0x00), then a DER SEQUENCE of { modulus, exponent }.
if (key_bits.len > 1) {
var rsa_inner = DERParser{ .buf = key_bits[1..], .allocator = allocator };
if (!rsa_inner.exhausted) {
const rsa_seq = try rsa_inner.readNode();
if (rsa_seq.children) |rk| {
if (rk.len > 0) {
const mod_bytes = rk[0].raw_value;
// First byte may be 0x00 padding for positive sign
const effective = if (mod_bytes[0] == 0x00) mod_bytes.len - 1 else mod_bytes.len;
key_size = effective * 8;
}
}
}
}
} else if (std.mem.eql(u8, alg_oid, "1.2.840.10045.2.1")) {
// ECDSA: look at the curve OID parameter
if (alg.len >= 2) {
const curve_oid = try decodeOID(allocator, alg[1].raw_value);
if (EC_CURVE_NAMES.get(curve_oid)) |curve| {
key_algorithm = try std.fmt.allocPrint(allocator, "{s} ({s})", .{ key_algorithm, curve });
}
}
// EC public key: BIT STRING = 0x00 + uncompressed point;
// for P-256 the point is 65 bytes (0x04 + 32 + 32).
if (key_bits.len > 1) {
key_size = (key_bits.len - 1 - 1) * 8; // drop 0x00 + 0x04 prefix → bits
}
} else {
key_size = if (key_bits.len > 1) (key_bits.len - 1) * 8 else 0;
}
}
}
}
}
// Extensions — look for context [3] after subjectPublicKeyInfo
var extensions = Extensions{};
while (idx < kids.len) : (idx += 1) {
const child = &kids[idx];
if (child.tag_class == .context and child.tag_number == 3 and child.children != null) {
const outer_seq = child.children.?[0];
if (outer_seq.children) |ext_entries| {
extensions = try parseExtensions(allocator, ext_entries);
}
}
}
// Serial number as "AA:BB:…" uppercase hex, dropping a 0x00 pad byte.
var serial_hex = std.ArrayList(u8).init(allocator);
if (serial_node.raw_value.len > 0) {
var serial_bytes = serial_node.raw_value;
if (serial_bytes[0] == 0x00 and serial_bytes.len > 1) serial_bytes = serial_bytes[1..];
for (serial_bytes, 0..) |b, i| {
if (i > 0) try serial_hex.append(':');
try serial_hex.writer().print("{X:0>2}", .{b});
}
}
return .{
.subject = subject,
.subject_dn = try formatDN(allocator, subject),
.issuer = issuer,
.issuer_dn = try formatDN(allocator, issuer),
.not_before = not_before,
.not_after = not_after,
.serial_number = try serial_hex.toOwnedSlice(),
.signature_algorithm = oidName(sig_alg_oid),
.key_algorithm = key_algorithm,
.key_size = key_size,
.basic_constraints_ca = extensions.basic_constraints_ca,
.key_usage = extensions.key_usage orelse &.{},
.ext_key_usage = extensions.ext_key_usage orelse &.{},
.san_dns = extensions.san_dns,
.san_ip = extensions.san_ip,
.san_email = extensions.san_email,
.version = version,
.raw_der = der,
};
}
/// True when the certificate's not-after instant is in the past.
pub fn isExpired(cert: *const CertificateInfo, now_epoch: i64) bool {
return cert.not_after < now_epoch;
}
/// Whole days from `now_epoch` until expiry (negative once expired).
pub fn daysUntilExpiry(cert: *const CertificateInfo, now_epoch: i64) i64 {
const diff_s = cert.not_after - now_epoch;
return @divTrunc(diff_s + 86399, 86400); // ceil toward +∞, like Math.ceil(ms/day)
}
/// Extract all PEM certificate blocks and return info for each.
pub fn decodeCertificateChain(allocator: std.mem.Allocator, pem: []const u8) Error![]CertificateInfo {
if (std.mem.trim(u8, pem, " \t\r\n").len == 0) return Error.EmptyInput;
const blocks = try extractPemBlocks(allocator, pem);
defer allocator.free(blocks);
if (blocks.len == 0) return Error.NoPemBlock;
var infos = std.ArrayList(CertificateInfo).init(allocator);
errdefer infos.deinit();
for (blocks) |der| {
try infos.append(try decodeDER(allocator, der));
}
return infos.toOwnedSlice();
}
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 →