Skip to content

ASN.1 and DER (Abstract Syntax Notation One, Distinguished Encoding Rules)

The schema language and byte format under certificates, keys and signed messages. ASN.1 says which fields a structure has; encoding rules turn values into bytes. A signature covers bytes, so cryptography uses DER, the rules that give every value exactly one encoding.

Standard: ITU-T X.680 (notation) and X.690 (BER, CER, DER), so numbered since 1994, current editions 2021, also ISO/IEC 8824-1 and 8825-1; first CCITT X.409 (1984), then X.208/X.209 (1988). X.509's modules: RFC 5280 Appendix A (1988 syntax), RFC 5912 (2010, 2002 syntax). Used by: X.509, key files, PKCS#7, PKCS#12, OCSP, TLS, S/MIME, Kerberos, ECDSA signatures in FIDO2 and Bitcoin.

One certificate, byte by byte

OpenSSL 3.0.13 and xxd on Ubuntu 24.04 in a container, Oct 2026, on ISRG Root X1, Let's Encrypt's root (trimmed; hl is tag plus length bytes):

$ openssl x509 -in /etc/ssl/certs/ISRG_Root_X1.pem -outform DER | xxd -l 48
00000000: 3082 056b 3082 0353 a003 0201 0202 1100  0..k0..S........
00000010: 8210 cfb0 d240 e359 4463 e0bb 6382 8b00  .....@.YDc..c...
00000020: 300d 0609 2a86 4886 f70d 0101 0b05 0030  0...*.H........0
$ openssl asn1parse -in /etc/ssl/certs/ISRG_Root_X1.pem -i
    0:d=0  hl=4 l=1387 cons: SEQUENCE
    4:d=1  hl=4 l= 851 cons:  SEQUENCE
    8:d=2  hl=2 l=   3 cons:   cont [ 0 ]
   10:d=3  hl=2 l=   1 prim:    INTEGER           :02
   13:d=2  hl=2 l=  17 prim:   INTEGER           :8210CFB0D240E3594463E0BB63828B00
…
  802:d=5  hl=2 l=   1 prim:      BOOLEAN           :255
Bytes ASN.1 What it is
30 82 05 6b, 30 82 03 53 SEQUENCE, SEQUENCE the Certificate: 30 is SEQUENCE (16) plus the constructed bit, 82 puts the length in the next two bytes, 05 6b = 1,387 (1,391 with the header). Inside it, tbsCertificate (851 bytes): exactly what the signature covers (RFC 5280 §4.1.1.3)
a0 03 02 01 02 [0] EXPLICIT INTEGER version: a0 is context-specific tag 0, constructed; 2 means v3. A v1 certificate leaves it out (DEFAULT v1)
02 11 00 82 10 … 8b 00 INTEGER serial: 16 bytes plus a 00, as 82 has its top bit set and would read as negative. RFC 5280 §4.1.2.2: positive, at most 20 bytes
30 0d 06 09 2a 86 … 05 00 AlgorithmIdentifier 1.2.840.113549.1.1.11 and NULL parameters: a signature by RSA PKCS#1 v1.5 over a SHA-256 hash
30 4f 31 0b 30 09 06 03 55 04 06 13 02 55 53, 30 1e 17 0d … Name, Validity issuer, a SEQUENCE of SETs starting with countryName (2.5.4.6) = PrintableString "US"; then two UTCTimes, 2015-06-04 and 2035-06-04 (from 2050: GeneralizedTime, RFC 5280 §4.1.2.5)
30 82 02 22 … 03 82 02 0f 00 SubjectPublicKeyInfo rsaEncryption (1.2.840.113549.1.1.1) and a BIT STRING (00: no unused bits) holding more DER: the 4096-bit modulus as a 513-byte INTEGER, and 65537
a3 42 … 01 01 ff [3] Extensions key usage and basic constraints, both critical (BOOLEAN ff), and a key identifier; see X.509
03 82 02 01 00 … BIT STRING the signature: an unused-bits byte, then 512 bytes of RSA-4096

"DER" twice. DER is the encoding; a .der (often .cer) file is called DER because it holds exactly these bytes. PEM is the same bytes in Base64, and the SHA-256 fingerprint is their hash. Files are on File formats.

How it is built

ASN.1 is a schema language, nearer Protocol Buffers, Thrift or SQL DDL than JSON: RFC 5280 §4.1 declares a Certificate as a SEQUENCE of tbsCertificate, signatureAlgorithm and signatureValue, and says nothing about bytes. BER, CER and DER write each value as tag, length, value (TLV):

  1. Tag. One byte up to tag 30: bits 8-7 the class (00 universal, 01 application, 10 context-specific, 11 private), bit 6 set if it holds more TLVs (constructed), bits 5-1 the number. Universal: BOOLEAN 1, INTEGER 2, BIT STRING 3, OCTET STRING 4, NULL 5, OID 6, UTF8String 12, SEQUENCE 16 (30), SET 17 (31), PrintableString 19, UTCTime 23, GeneralizedTime 24. [0] EXPLICIT wraps the inner TLV in a0; [0] IMPLICIT replaces its tag.
  2. Length. 0 to 127 in one byte; else 81 to 84 give the number of length bytes. 80 alone is indefinite, ended by 00 00: BER only.
  3. Value. INTEGER is two's complement in the fewest bytes, so 127 is 02 01 7f and 128 is 02 02 00 80. RSA-4096 moduli take 513 bytes, and ECDSA's \(r\) and \(s\) each gain a 00 half the time, so 400 P-256 signatures here ran from 69 to 72 bytes, mostly 71. JWT and DNSSEC send \(r\) and \(s\) raw, 64 bytes.
  4. OIDs name algorithms, attributes and extensions in one global tree. The first two arcs share a byte, \(40 \cdot X + Y\); later arcs are base 128, top bit set on all but the last byte: 2a 86 48 86 f7 0d … is 1.2 (42), 840 (\(6 \cdot 128 + 72\)), …
OID Bytes after 06 len Name Defined in
1.2.840.113549.1.1.11 2a 86 48 86 f7 0d 01 01 0b sha256WithRSAEncryption: RSA PKCS#1 v1.5, SHA-256 RFC 4055, RFC 8017
1.2.840.10045.4.3.2 2a 86 48 ce 3d 04 03 02 ecdsa-with-SHA256: ECDSA RFC 5758
1.3.101.112 2b 65 70 id-Ed25519: EdDSA RFC 8410
2.16.840.1.101.3.4.3.17 60 86 48 01 65 03 04 03 11 id-ml-dsa-44: ML-DSA; OpenSSL names it from 3.5 (2025), 3.0 prints the number NIST arc; RFC 9881 (2025)

BER, DER and the other rules

DER is BER with the choices removed (X.690 §10-11): definite lengths in the fewest bytes (30 0d, never 30 81 0d or 30 80 … 00 00), strings in one primitive piece, BOOLEAN true only as ff, DEFAULT values left out, SET OF sorted by encoding. Fewest-byte INTEGERs and OID arcs are already BER rules.

Why one encoding. A verifier that accepts several byte strings for one value gives an attacker room to hide things. In 2006 Bleichenbacher forged RSA signatures with a cube root (e = 3) against verifiers that ignored bytes after the hash (CVE-2006-4339, fixed in OpenSSL 0.9.8c). In 2014 "BERserk" hid such bytes in over-long lengths in NSS; NSS 3.17.1 now builds the expected DER and compares bytes, as RFC 8017 §8.2.2 does, and 3.17.3 rejects padded lengths (CVE-2014-1569). OpenSSL 1.0.1k (2015) rejects ECDSA signatures that do not re-encode to the same bytes (CVE-2014-8275), which split Bitcoin nodes and led to strict DER (BIP-66).

Rules Standard Bytes Where you meet them
BER X.690 TLV, several encodings per value CMS and PKCS#12, built for streaming (RFC 5652 §2, RFC 7292 §4): openssl cms -sign -stream and NSS's pk12util wrote 30 80 here, openssl pkcs12 and keytool did not; LDAP (RFC 4511 §5.1), SNMP
CER X.690 canonical: indefinite lengths, strings in 1,000-byte pieces rarely
DER X.690 canonical: definite lengths X.509, keys, OCSP, Kerberos, CMS signed attributes, ECDSA signatures
PER: aligned, unaligned (UPER), canonical (CPER) X.691 packed bits, no tags 4G and 5G signalling: RRC unaligned, S1AP and NGAP aligned
OER, canonical OER (COER) X.696 (2014) whole bytes, lengths only where sizes vary IEEE 1609.2 / ETSI TS 103 097 vehicle (V2X) certificates
XER, canonical (CXER), extended (E-XER); JER; GSER X.693; X.697 (2017); RFC 3641 (2003) XML; JSON; text rarely; GSER in LDAP

Seeing it on your machine

openssl x509 -in /etc/ssl/certs/ISRG_Root_X1.pem -outform DER -out isrg.der  # PEM to DER
openssl asn1parse -genstr INTEGER:128 -noout -out - | xxd -p     # 02020080
openssl asn1parse -genconf <(printf 'asn1=OID:1.3.101.112\n') -noout -out - | xxd -p  # 06032b6570
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out ec.key
echo hi | openssl dgst -sha256 -sign ec.key | openssl asn1parse -inform DER  # SEQUENCE { r, s }
  • Python: a mini asn1parse. Press Enter for ISRG Root X1, or give another PEM file or Base64 (MA0GCSqGSIb3DQEBCwUA is the AlgorithmIdentifier above).
import base64, os, re

src = input("PEM file or base64: ").strip() or "/etc/ssl/certs/ISRG_Root_X1.pem"
text = open(src).read() if os.path.exists(src) else src
m = re.search(r"-----BEGIN [^-]+-----(.+?)-----END", text, re.S)      # first PEM block, if any
der = base64.b64decode("".join((m.group(1) if m else text).split()))
NAMES = {1: "BOOLEAN", 2: "INTEGER", 3: "BIT STRING", 4: "OCTET STRING", 5: "NULL", 6: "OBJECT",
         12: "UTF8String", 16: "SEQUENCE", 17: "SET", 19: "PrintableString", 23: "UTCTime"}

def oid(body):                     # base-128 arcs, high bit = more to come; first arc = 40*X + Y
    arcs, n = [], 0
    for b in body:
        n = n << 7 | b & 0x7F
        if b < 0x80:
            arcs.append(n); n = 0
    x = min(arcs[0] // 40, 2)
    return ".".join(map(str, [x, arcs[0] - 40 * x] + arcs[1:]))

def walk(start, end, depth, max_depth=2):
    while start < end:
        tag, n, hl = der[start], der[start + 1], 2
        if n == 0x80:
            raise ValueError("indefinite length: BER, not DER")
        if n > 0x80:               # long form: low 7 bits say how many length octets follow
            hl += n & 0x7F
            n = int.from_bytes(der[start + 2:start + hl], "big")
        body = der[start + hl:start + hl + n]
        name = NAMES.get(tag & 0x1F, "?") if tag < 0x40 else f"[{tag & 0x1F}]"  # not universal
        value = oid(body) if tag == 6 else body.hex() if tag in (1, 2) else ""
        print(f"{start:4} {tag:02x} l={n:<4} {'  ' * depth}{name} {value}".rstrip())
        if tag & 0x20 and depth < max_depth:                    # constructed: walk its contents
            walk(start + hl, start + hl + n, depth + 1)
        start += hl + n

walk(0, len(der), 0)
   0 30 l=1387 SEQUENCE
   4 30 l=851    SEQUENCE
   8 a0 l=3        [0]
  13 02 l=17       INTEGER 008210cfb0d240e3594463e0bb63828b00
  32 30 l=13       SEQUENCE
  47 30 l=79       SEQUENCE
 128 30 l=30       SEQUENCE
 160 30 l=79       SEQUENCE
 241 30 l=546      SEQUENCE
 791 a3 l=66       [3]
 859 30 l=13     SEQUENCE
 861 06 l=9        OBJECT 1.2.840.113549.1.1.11
 872 05 l=0        NULL
 874 03 l=513    BIT STRING

Depth 2 is tbsCertificate's fields, at asn1parse's offsets. With max_depth=9 all 12 OIDs sit where OpenSSL prints their names (2.5.4.6 countryName, …); streamed CMS stops it at 80. More commands: openssl.

See also

  • X.509: what each certificate field means
  • File formats: DER and PEM files, PKCS#7, PKCS#12 and Java key stores
  • PKI: chains, CAs and trust stores
  • RSA / ECDSA: the signatures whose bytes DER pins down

References

Weaker sources: search-engine summaries of ITU-T's pages (07/1994 and 02/2021 editions, ISO/IEC 8824-1/8825-1:2021, X.696 08/2014, X.697 10/2017), RFC 9881's date, CERT/CC and SecurityWeek on BERserk (CVE-2014-1568), IEEE 1609.2's COER.