Skip to content

Certificate Decoder — Java 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 Java implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Certificate Decoder — pure ASN.1 DER parser + X.509 certificate decoder.
//
// Language: Java (17+, standard library only)
// Ported from src/lib/cert-decoder.ts
// display source — part of CosmoDev's polyglot tool pages.
//
// No external dependencies — DER is deterministic and parsed sequentially.

import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public final class CertDecoder {

    // ── OID name map ───────────────────────────────────────────────────────────

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

    /** EC named-curve OIDs. */
    public static final Map<String, String> EC_CURVE_NAMES = Map.of(
        "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. */
    private static final List<String> KEY_USAGE_BITS = List.of(
        "digitalSignature", "nonRepudiation", "keyEncipherment", "dataEncipherment",
        "keyAgreement", "keyCertSign", "cRLSign", "encipherOnly", "decipherOnly");

    // ── ASN.1 DER types ────────────────────────────────────────────────────────

    public enum TagClass { UNIVERSAL, CONTEXT, APPLICATION, PRIVATE }

    public static final class Asn1Node {
        public final TagClass tagClass;
        public final boolean constructed;
        public final int tagNumber;
        public final byte[] rawValue; // value bytes (decoded content)
        public final int offset;      // offset into original buffer
        public final List<Asn1Node> children; // null for primitive nodes

        Asn1Node(TagClass tagClass, boolean constructed, int tagNumber,
                 byte[] rawValue, int offset, List<Asn1Node> children) {
            this.tagClass = tagClass;
            this.constructed = constructed;
            this.tagNumber = tagNumber;
            this.rawValue = rawValue;
            this.offset = offset;
            this.children = children;
        }
    }

    // ── DER parsing helpers ────────────────────────────────────────────────────

    static final class DERParser {
        private final byte[] buf;
        private int pos;

        DERParser(byte[] buf) {
            this.buf = buf;
        }

        boolean exhausted() {
            return pos >= buf.length;
        }

        /** Read the next TLV node. */
        Asn1Node readNode() {
            int offset = pos;
            int byte0 = readByte() & 0xff;

            TagClass tagClass =
                (byte0 & 0xc0) == 0x00 ? TagClass.UNIVERSAL
                : (byte0 & 0xc0) == 0x40 ? TagClass.APPLICATION
                : (byte0 & 0xc0) == 0x80 ? TagClass.CONTEXT
                : TagClass.PRIVATE;

            boolean constructed = (byte0 & 0x20) != 0;
            int tagNumber = byte0 & 0x1f;

            // Long-form tag (tag number >= 31)
            if (tagNumber == 0x1f) {
                tagNumber = 0;
                int b;
                do {
                    b = readByte() & 0xff;
                    tagNumber = (tagNumber << 7) | (b & 0x7f);
                } while ((b & 0x80) != 0);
            }

            int length = readLength();

            if (pos + length > buf.length) {
                throw new IllegalArgumentException("Truncated DER: need " + length + " bytes at offset "
                    + pos + ", but only " + (buf.length - pos) + " remain");
            }

            byte[] rawValue = Arrays.copyOfRange(buf, pos, pos + length);
            pos += length;

            List<Asn1Node> children = null;
            // Parse children for constructed types
            if (constructed && rawValue.length > 0) {
                DERParser childParser = new DERParser(rawValue);
                children = new ArrayList<>();
                while (!childParser.exhausted()) {
                    children.add(childParser.readNode());
                }
            }

            return new Asn1Node(tagClass, constructed, tagNumber, rawValue, offset, children);
        }

        private int readByte() {
            if (pos >= buf.length) {
                throw new IllegalArgumentException("Unexpected end of DER data");
            }
            return buf[pos++];
        }

        private int readLength() {
            int first = readByte() & 0xff;
            if (first < 0x80) return first;

            int numBytes = first & 0x7f;
            if (numBytes == 0) {
                throw new IllegalArgumentException("Indefinite length is not supported in DER");
            }
            if (numBytes > 4) {
                throw new IllegalArgumentException("Length too large: " + numBytes + " bytes");
            }
            int len = 0;
            for (int i = 0; i < numBytes; i++) {
                len = (len << 8) | (readByte() & 0xff);
            }
            return len;
        }
    }

    // ── OID decoding ──────────────────────────────────────────────────────────

    private static String decodeOID(byte[] bytes) {
        if (bytes.length == 0) throw new IllegalArgumentException("Empty OID");
        List<String> parts = new ArrayList<>();
        parts.add(String.valueOf((bytes[0] & 0xff) / 40));
        parts.add(String.valueOf((bytes[0] & 0xff) % 40));

        long value = 0;
        for (int i = 1; i < bytes.length; i++) {
            int b = bytes[i] & 0xff;
            value = (value << 7) | (b & 0x7f);
            if ((b & 0x80) == 0) {
                parts.add(String.valueOf(value));
                value = 0;
            }
        }
        return String.join(".", parts);
    }

    private static String oidName(String oid) {
        return OID_NAMES.getOrDefault(oid, oid);
    }

    // ── RDN (Relative Distinguished Name) helpers ──────────────────────────────

    public record RDNAttribute(String type, String value) {
    }

    private static List<RDNAttribute> parseRDN(Asn1Node node) {
        List<RDNAttribute> attrs = new ArrayList<>();
        if (node.children == null) return attrs;

        for (Asn1Node rdnSet : node.children) {
            if (rdnSet.children == null) continue;
            for (Asn1Node attrSeq : rdnSet.children) {
                if (attrSeq.children == null || attrSeq.children.size() < 2) continue;
                String oid = decodeOID(attrSeq.children.get(0).rawValue);
                attrs.add(new RDNAttribute(oidName(oid), readString(attrSeq.children.get(1))));
            }
        }
        return attrs;
    }

    private static String formatDN(List<RDNAttribute> attrs) {
        List<String> parts = new ArrayList<>();
        for (RDNAttribute a : attrs) parts.add(a.type() + "=" + a.value());
        return String.join(", ", parts);
    }

    private static String readString(Asn1Node node) {
        int tag = node.tagNumber;
        if (tag == 12 || tag == 19 || tag == 22 || tag == 30 || tag == 36) {
            // UTF8String(12), PrintableString(19), IA5String(22), UTF8String variant,
            // BMPString(30), etc. — all decode as text (BMPString approximated as UTF-16
            // in the TS reference via TextDecoder; here plain UTF-8 like the reference's
            // first branch).
            return new String(node.rawValue, StandardCharsets.UTF_8);
        }
        // Fallback: try strict UTF-8; last resort, colon-separated hex.
        try {
            return StandardCharsets.UTF_8.newDecoder().decode(java.nio.ByteBuffer.wrap(node.rawValue)).toString();
        } catch (Exception e) {
            StringBuilder hex = new StringBuilder();
            for (byte b : node.rawValue) hex.append(String.format("%02x", b)).append(':');
            return hex.substring(0, hex.length() - 1);
        }
    }

    // ── Time helpers ───────────────────────────────────────────────────────────

    private static final Pattern UTCTIME_RE = Pattern.compile("^(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})Z$");
    private static final Pattern GENTIME_RE = Pattern.compile("^(\\d{4})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})Z$");

    private static Instant parseTime(Asn1Node node) {
        String str = readString(node).trim();
        Matcher m;
        // UTCTime (tag 23): YYMMDDHHMMSSZ
        if (node.tagNumber == 23 && (m = UTCTIME_RE.matcher(str)).matches()) {
            int year = Integer.parseInt(m.group(1));
            year += year >= 50 ? 1900 : 2000;
            return LocalDateTime.of(year, Integer.parseInt(m.group(2)), Integer.parseInt(m.group(3)),
                Integer.parseInt(m.group(4)), Integer.parseInt(m.group(5)), Integer.parseInt(m.group(6)))
                .toInstant(ZoneOffset.UTC);
        }
        // GeneralizedTime (tag 24): YYYYMMDDHHMMSSZ
        if (node.tagNumber == 24 && (m = GENTIME_RE.matcher(str)).matches()) {
            return LocalDateTime.of(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)),
                Integer.parseInt(m.group(3)), Integer.parseInt(m.group(4)),
                Integer.parseInt(m.group(5)), Integer.parseInt(m.group(6)))
                .toInstant(ZoneOffset.UTC);
        }
        throw new IllegalArgumentException("Unknown time tag: " + node.tagNumber);
    }

    // ── Extension parsing ──────────────────────────────────────────────────────

    private static final class Extensions {
        Boolean basicConstraintsCA;
        List<String> keyUsage;
        List<String> extKeyUsage;
        List<String> sanDNS = new ArrayList<>();
        List<String> sanIP = new ArrayList<>();
        List<String> sanEmail = new ArrayList<>();
    }

    private static Extensions parseExtensions(List<Asn1Node> nodes) {
        Extensions ext = new Extensions();
        for (Asn1Node extNode : nodes) {
            if (extNode.children == null || extNode.children.size() < 2) continue;
            String oid = decodeOID(extNode.children.get(0).rawValue);

            // Skip critical boolean if present
            int valueIdx = 1;
            if (extNode.children.size() >= 3
                    && extNode.children.get(1).tagNumber == 1
                    && extNode.children.get(1).tagClass == TagClass.UNIVERSAL) {
                valueIdx = 2;
            }

            byte[] octetContent = extNode.children.get(valueIdx).rawValue;
            if (octetContent.length == 0) continue;

            DERParser inner = new DERParser(octetContent);
            if (inner.exhausted()) continue;
            Asn1Node content = inner.readNode();

            switch (oid) {
                case "2.5.29.19" -> { // Basic Constraints
                    ext.basicConstraintsCA = false;
                    if (content.children != null && !content.children.isEmpty()) {
                        Asn1Node boolVal = content.children.get(0);
                        if (boolVal.tagNumber == 1 && boolVal.rawValue.length == 1
                                && (boolVal.rawValue[0] & 0xff) == 0xff) {
                            ext.basicConstraintsCA = true;
                        }
                    }
                }
                case "2.5.29.15" -> ext.keyUsage = parseBitString(content); // Key Usage — BIT STRING
                case "2.5.29.37" -> { // Extended Key Usage
                    ext.extKeyUsage = new ArrayList<>();
                    if (content.children != null) {
                        for (Asn1Node child : content.children) {
                            ext.extKeyUsage.add(oidName(decodeOID(child.rawValue)));
                        }
                    }
                }
                case "2.5.29.17" -> { // Subject Alternative Name
                    if (content.children != null) {
                        for (Asn1Node child : content.children) {
                            if (child.tagClass != TagClass.CONTEXT) continue;
                            if (child.tagNumber == 2) { // DNS
                                ext.sanDNS.add(readString(child));
                            } else if (child.tagNumber == 7) { // IP
                                byte[] ip = child.rawValue;
                                if (ip.length == 4) {
                                    ext.sanIP.add((ip[0] & 0xff) + "." + (ip[1] & 0xff) + "."
                                        + (ip[2] & 0xff) + "." + (ip[3] & 0xff));
                                } else if (ip.length == 16) {
                                    // IPv6 — standard hex representation
                                    String[] parts = new String[8];
                                    for (int i = 0; i < 16; i += 2) {
                                        parts[i / 2] = Integer.toHexString(((ip[i] & 0xff) << 8) | (ip[i + 1] & 0xff));
                                    }
                                    ext.sanIP.add(String.join(":", parts));
                                }
                            } else if (child.tagNumber == 1) { // Email
                                ext.sanEmail.add(readString(child));
                            }
                        }
                    }
                }
                default -> { /* other extensions are not surfaced */ }
            }
        }
        return ext;
    }

    private static List<String> parseBitString(Asn1Node node) {
        if (node.rawValue.length < 2) return List.of();
        int unusedBits = node.rawValue[0] & 0xff;
        byte[] octets = Arrays.copyOfRange(node.rawValue, 1, node.rawValue.length);
        List<String> flags = new ArrayList<>();

        for (int i = 0; i < KEY_USAGE_BITS.size(); i++) {
            int octetIdx = i / 8;
            int bitIdx = 7 - (i % 8);
            if (octetIdx < octets.length && (octets[octetIdx] & (1 << bitIdx)) != 0) {
                flags.add(KEY_USAGE_BITS.get(i));
            }
        }

        // Mask out unused bits
        if (unusedBits > 0 && !flags.isEmpty()) {
            int totalBits = octets.length * 8 - unusedBits;
            while (flags.size() > totalBits) {
                flags.remove(flags.size() - 1);
            }
        }
        return flags;
    }

    // ── Public types ────────────────────────────────────────────────────────────

    public record CertificateInfo(
        List<RDNAttribute> subject, String subjectDN,
        List<RDNAttribute> issuer, String issuerDN,
        Instant notBefore, Instant notAfter,
        /** Serial number as colon-separated uppercase hex. */
        String serialNumber,
        /** Signature algorithm (human-readable name). */
        String signatureAlgorithm,
        String keyAlgorithm, int keySize,
        /** Basic Constraints CA flag (null when the extension is absent). */
        Boolean basicConstraintsCA,
        List<String> keyUsage, List<String> extKeyUsage,
        List<String> sanDNS, List<String> sanIP, List<String> sanEmail,
        /** Version number (0=v1, 1=v2, 2=v3). */
        int version,
        /** Raw DER bytes (for fingerprinting). */
        byte[] rawDER) {
    }

    // ── PEM handling ───────────────────────────────────────────────────────────

    private static final Pattern PEM_REGEX = Pattern.compile(
        "-----BEGIN\\s+CERTIFICATE-----\\s*\\r?\\n([\\s\\S]*?)\\r?\\n-----END\\s+CERTIFICATE-----");

    private static List<String> pemBlocks(String pem) {
        List<String> blocks = new ArrayList<>();
        Matcher match = PEM_REGEX.matcher(pem);
        while (match.find()) blocks.add(match.group(1));
        return blocks;
    }

    private static byte[] pemToDER(String pem) {
        // Strip whitespace and decode base64
        String b64 = pem.replaceAll("\\s", "");
        return Base64.getMimeDecoder().decode(b64);
    }

    // ── Main decoder ───────────────────────────────────────────────────────────

    public static CertificateInfo decodeCertificate(String pem) {
        if (pem == null || pem.trim().isEmpty()) {
            throw new IllegalArgumentException("Empty input — paste a PEM certificate");
        }

        List<String> blocks = pemBlocks(pem);
        if (blocks.isEmpty()) {
            throw new IllegalArgumentException(
                "No PEM certificate block found — expected -----BEGIN CERTIFICATE-----");
        }

        // Decode the first certificate
        byte[] der = pemToDER(blocks.get(0));

        // Parse outer SEQUENCE
        Asn1Node certSeq = new DERParser(der).readNode();
        if (certSeq.children == null || certSeq.children.size() < 3) {
            throw new IllegalArgumentException(
                "Invalid certificate structure: expected TBSCertificate, signatureAlgorithm, signatureValue");
        }

        Asn1Node tbs = certSeq.children.get(0);
        Asn1Node sigAlgNode = certSeq.children.get(1);
        String sigAlgOID = sigAlgNode.children != null
            ? decodeOID(sigAlgNode.children.get(0).rawValue) : "";

        // Parse TBSCertificate
        if (tbs.children == null || tbs.children.size() < 7) {
            throw new IllegalArgumentException("Invalid TBSCertificate structure");
        }

        int idx = 0;

        // Version (explicit context [0])
        int version = 0; // default v1
        if (tbs.children.get(idx).tagClass == TagClass.CONTEXT && tbs.children.get(idx).tagNumber == 0) {
            List<Asn1Node> versionChildren = tbs.children.get(idx).children;
            if (versionChildren != null && !versionChildren.isEmpty()
                    && versionChildren.get(0).rawValue.length == 1) {
                version = versionChildren.get(0).rawValue[0] & 0xff;
            }
            idx++;
        }

        // Serial Number
        Asn1Node serialNode = tbs.children.get(idx++);

        idx++; // skip inner sig algorithm

        // Issuer
        List<RDNAttribute> issuer = parseRDN(tbs.children.get(idx++));

        // Validity
        Asn1Node validityNode = tbs.children.get(idx++);
        Instant notBefore = Instant.ofEpochMilli(0);
        Instant notAfter = Instant.ofEpochMilli(0);
        if (validityNode.children != null && validityNode.children.size() >= 2) {
            notBefore = parseTime(validityNode.children.get(0));
            notAfter = parseTime(validityNode.children.get(1));
        }

        // Subject
        List<RDNAttribute> subject = parseRDN(tbs.children.get(idx++));

        // SubjectPublicKeyInfo
        Asn1Node spkiNode = tbs.children.get(idx++);
        String keyAlgorithm = "unknown";
        int keySize = 0;

        if (spkiNode.children != null && spkiNode.children.size() >= 2) {
            Asn1Node algSeq = spkiNode.children.get(0);
            byte[] keyBits = spkiNode.children.get(1).rawValue;

            if (algSeq.children != null && !algSeq.children.isEmpty()) {
                String algOID = decodeOID(algSeq.children.get(0).rawValue);
                keyAlgorithm = oidName(algOID);

                // Key size estimation
                if (algOID.equals("1.2.840.113549.1.1.1")) {
                    // RSA: the BIT STRING value starts with an unused-bits byte (0x00),
                    // then a DER-encoded SEQUENCE of { modulus INTEGER, exponent INTEGER }.
                    if (keyBits.length > 1) {
                        DERParser rsaInner = new DERParser(Arrays.copyOfRange(keyBits, 1, keyBits.length));
                        if (!rsaInner.exhausted()) {
                            Asn1Node rsaSeq = rsaInner.readNode();
                            if (rsaSeq.children != null && !rsaSeq.children.isEmpty()) {
                                byte[] modBytes = rsaSeq.children.get(0).rawValue;
                                // First byte may be 0x00 padding for positive sign
                                keySize = ((modBytes[0] & 0xff) == 0x00
                                    ? modBytes.length - 1 : modBytes.length) * 8;
                            }
                        }
                    }
                } else if (algOID.equals("1.2.840.10045.2.1")) {
                    // ECDSA: look at the curve OID parameter
                    if (algSeq.children.size() >= 2) {
                        String curveOID = decodeOID(algSeq.children.get(1).rawValue);
                        String curveName = EC_CURVE_NAMES.get(curveOID);
                        if (curveName != null) keyAlgorithm = keyAlgorithm + " (" + curveName + ")";
                    }
                    // EC public key: BIT STRING value = 0x00 (unused) + uncompressed point.
                    // Key "size" = curve order bits.
                    if (keyBits.length > 1) {
                        keySize = (keyBits.length - 1) * 8; // subtract 0x04 prefix, byte count → bits
                    }
                } else {
                    keySize = keyBits.length > 1 ? (keyBits.length - 1) * 8 : 0;
                }
            }
        }

        // Extensions — look for context [3] after subjectPublicKeyInfo
        Extensions extensions = new Extensions();
        while (idx < tbs.children.size()) {
            Asn1Node child = tbs.children.get(idx);
            if (child.tagClass == TagClass.CONTEXT && child.tagNumber == 3 && child.children != null) {
                // context [3] wraps a single outer SEQUENCE of Extension entries
                Asn1Node outerSeq = child.children.get(0);
                if (outerSeq != null && outerSeq.children != null) {
                    extensions = parseExtensions(outerSeq.children);
                }
            }
            idx++;
        }

        // Serial number as colon-separated uppercase hex, minus sign padding
        String serialHex = "";
        if (serialNode.rawValue.length > 0) {
            byte[] serialBytes = serialNode.rawValue;
            if ((serialBytes[0] & 0xff) == 0x00 && serialBytes.length > 1) {
                serialBytes = Arrays.copyOfRange(serialBytes, 1, serialBytes.length);
            }
            StringBuilder sb = new StringBuilder();
            for (byte b : serialBytes) sb.append(String.format("%02X", b)).append(':');
            serialHex = sb.substring(0, sb.length() - 1);
        }

        return new CertificateInfo(
            subject, formatDN(subject),
            issuer, formatDN(issuer),
            notBefore, notAfter,
            serialHex,
            oidName(sigAlgOID),
            keyAlgorithm, keySize,
            extensions.basicConstraintsCA,
            extensions.keyUsage == null ? List.of() : extensions.keyUsage,
            extensions.extKeyUsage == null ? List.of() : extensions.extKeyUsage,
            extensions.sanDNS, extensions.sanIP, extensions.sanEmail,
            version, der);
    }

    public static boolean isExpired(CertificateInfo cert) {
        return cert.notAfter().isBefore(Instant.now());
    }

    public static long daysUntilExpiry(CertificateInfo cert) {
        long diffMs = cert.notAfter().toEpochMilli() - System.currentTimeMillis();
        return (long) Math.ceil(diffMs / (1000.0 * 60 * 60 * 24));
    }

    /** Extract all PEM certificate blocks and return info for each. */
    public static List<CertificateInfo> decodeCertificateChain(String pem) {
        if (pem == null || pem.trim().isEmpty()) {
            throw new IllegalArgumentException("Empty input — paste PEM certificate(s)");
        }
        List<String> blocks = pemBlocks(pem);
        if (blocks.isEmpty()) {
            throw new IllegalArgumentException(
                "No PEM certificate block found — expected -----BEGIN CERTIFICATE-----");
        }
        List<CertificateInfo> out = new ArrayList<>();
        for (String block : blocks) {
            out.add(decodeCertificate(
                "-----BEGIN CERTIFICATE-----\n" + block + "\n-----END CERTIFICATE-----"));
        }
        return out;
    }

    private CertDecoder() {
    }
}

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 →