Low-Level ASN.1 API
This page is the detailed reference for awesn1 low-level APIs.
Use this page when you work directly with ASN.1 elements, tags, DER bytes, and primitive payload encoding/decoding.
For the data-class-first flow, see the Serialization Tutorial.
ASN.1 Model: Primitives vs Structures
awesn1 distinguishes two layers of abstraction:
- Generic ASN.1 elements:
Raw TLV nodes (
Asn1Element,Asn1Primitive,Asn1Structure) used when you parse bytes, inspect tags, assert structure, or build custom workflows. - Rich semantic types:
Domain-focused wrappers such as
Asn1Integer,Asn1Real,Asn1String,Asn1Time, andObjectIdentifierthat add type-specific behaviour on top of primitive bytes.
When doing low-level ASN.1 work, you usually parse/build with generic elements first, then convert to semantic types where needed. See ASN.1-Specific Rich Types.
More often than not, awesn1's first-class kotlinx-serialization integration will serve you better, because almost everything can be modelled using plain kotlin types.
Base Model
Asn1Element: Base type for all ASN.1 nodes (hastag,contentLength/contentLengthLong,derEncoded, pretty-print support).Asn1Element.Tag: Encodes tag number, tag class, andCONSTRUCTEDbit.Asn1Primitive: A leaf element with rawcontent: ByteArray.Asn1Structure: A constructed element withchildren: List<Asn1Element>.
Structure Implementations
Asn1Sequence: ordered structure (SEQUENCE).Asn1Set: set semantics, DER sorting rules.Asn1SetOf: set semantics + same-tag constraint across children.Asn1ExplicitlyTagged: context-specific constructed wrapper.Asn1CustomStructure: custom-tagged structure (constructed or primitive semantics).Asn1EncapsulatingOctetString:OCTET STRINGthat contains ASN.1 children.
Octet Strings
Asn1OctetStringis the abstraction.Asn1PrimitiveOctetStringholds raw bytes.Asn1EncapsulatingOctetStringholds child ASN.1 elements while still tagged asOCTET STRING.
Deferred Semantic Parsing
awesn1 separates raw DER parsing from ASN.1 meaning. The parser first builds an Asn1Element tree from TLV bytes and
only enforces constraints needed to keep that raw tree well-bounded and DER-shaped. Type-specific validation happens
when you ask for semantics, for example by decoding a primitive as a boolean, integer, string, time, object identifier,
or custom data class.
At the raw parser layer, awesn1 rejects:
- indefinite length encoding, non-minimal long-form length encoding, length overflow, and child elements that overrun their parent
- high-tag-number encoding for tag numbers that belong in low-tag-number form (
<= 30) - universal tag
0(end-of-contents) and universal tag15
Everything else is intentionally deferred. For example, a constructed universal UTF8String is not treated as a valid
UTF8String, but it can still be represented as an Asn1CustomStructure if its children form a valid raw ASN.1 tree.
Likewise, a primitive with a tag whose content is not meaningful for that tag can still be inspected as raw bytes.
Examples for semantic decoders are where ASN.1 meaning becomes strict:
readNull()verifies thatNULLhas empty content.decodeToBoolean()requires one content byte and accepts only0x00or0xff.Asn1Integer.decodeFromAsn1ContentBytes()requires non-empty, minimally encoded INTEGER content.- String, time, bit-string, object-identifier, and custom
Asn1Decodableimplementations perform their own type-specific checks.
This is useful for real-world interoperability work. You can parse malformed or quirky input into a raw tree, preserve
it for diagnostics or signature checks, and then define a semantic model that decides how strict to be. For example,
a data class can keep a private raw primitive for a legacy field and expose a public getter that normalizes one-byte
non-standard boolean values, while stricter code can keep using decodeToBoolean().
ASN.1-Specific Rich Types
The core module includes rich semantic types beyond raw TLV primitives:
Asn1Integer: arbitrary-precision ASN.1 INTEGER handling.Asn1Real: ASN.1 REAL with arbitrary precision model and IEEE-754 bridges.Asn1Stringhierarchy: UTF8, Printable, IA5, BMP, Numeric, etc.Asn1Time: bridgesInstantto UTC/Generalized Time.Asn1BitString: bit-level representation with padding-bit tracking.BitSet: pure-Kotlin bitset implementation.ObjectIdentifier: OID support (string/components/bytes/UUID-based constructors). See Object Identifiers (OID) Deep Dive.
Object Identifiers (OID) Deep Dive
What an OID is
An ASN.1 OBJECT IDENTIFIER (OID) is a globally unique identifier in a hierarchical number tree, commonly written in dotted decimal notation:
1.2.840.1135492.5.4.31.3.6.1.5.5.7
Each number is called an arc (or node). The full path identifies exactly one object in the global OID namespace.
Why OIDs are Instrumental
OIDs are the backbone of many ASN.1-based ecosystems because they provide stable, interoperable identifiers for semantics, not just bytes. They are used to identify:
- algorithms (for example signature/hash algorithms)
- attributes (for example X.509 distinguished name fields)
- extensions (for example certificate extensions)
- protocol message/object types
- vendor/private namespaces
Without OIDs, two systems might parse the same ASN.1 structure but disagree on meaning.
In awesn1 specifically, OIDs are also central to open polymorphism by identifier; see the serialization tutorial section Open Polymorphism by OID.
Encoding Model (DER/BER content bytes)
ASN.1 OID content encoding is special:
- The first two arcs are folded into one value:
40 * arc0 + arc1. - Remaining arcs are encoded as base-128 varints.
- The ASN.1 tag is universal
OBJECT IDENTIFIER(0x06).
awesn1 handles this through ObjectIdentifier and its content-byte helpers.
For background on the historical encoding shape, see Microsoft's summary: About Object Identifier.
awesn1 Validation and Behaviour
Current ObjectIdentifier validation enforces:
- empty OIDs are rejected
- at least two arcs are required for node-based construction
- first arc must be
0,1, or2 - if first arc is
0or1, second arc must be< 40 - for current implementation constraints, first-byte-form parsing also limits accepted first-subidentifier values to the current supported range
For invalid inputs, constructors/decoders throw Asn1Exception/Asn1StructuralException.
Creating and Converting OIDs
ObjectIdentifier supports multiple entry points:
ObjectIdentifier(vararg nodes: UInt)for numeric arcsObjectIdentifier("1.2.840.113549")(also space-separated strings)ObjectIdentifier(uuid)for deterministic2.25.<uuid-as-integer>OIDs
Useful properties and conversions:
oid.bytes: ASN.1 OID content bytes (not full TLV)oid.nodes: lazily decoded arc listoid.toString(): dotted decimal form
Encoding/decoding APIs:
ObjectIdentifier.encodeToTlv(): wraps bytes inAsn1Primitive(Tag.OID, ...)ObjectIdentifier.decodeFromAsn1ContentBytes(bytes)Asn1Primitive.readOid()
Human-Readable Naming (KnownOIDs)
KnownOIDs is a mutable mapping from ObjectIdentifier to description strings. It is intended for diagnostics and developer-facing output, not wire semantics.
- Add custom descriptions with
KnownOIDs[oid] = "..." - If the
oidsmodule is on your classpath, callKnownOIDs.describeAll()to preload common names - Pretty-printing can use these names for easier debugging
Serialization Behaviour
ObjectIdentifier has dual behaviour:
- with DER codecs, it encodes/decodes as ASN.1
OBJECT IDENTIFIER - with non-DER serializers, it falls back to string form via
ObjectIdentifierStringSerializer
Raw ASN.1 Decoding
Typical Pipeline
- Parse DER bytes into
Asn1Element. - Assert expected tags/structure.
- Decode primitive content bytes into Kotlin/rich types, where semantic validation happens.
Parse Entry Points
Asn1Element.parse(source: ByteArray): Asn1ElementAsn1Element.parseAll(source: ByteArray): List<Asn1Element>Asn1Element.parseFirst(source: ByteArray): Pair<Asn1Element, ByteArray>Asn1Element.parseFromDerHexString(derEncoded: String): Asn1Element
The ByteArray entry points are bounded by the array's size; the streaming Source overloads require an explicit byte
limit. For untrusted input, read Hardening, Fuzzing & Robustness first.
Parsing empty input throws Asn1Exception (these never return null).
Tag assertion helpers:
Asn1Element.assertTag(tag: Asn1Element.Tag)Asn1Element.assertTag(tagNumber: ULong)
Example
High-level Decoding Contract (Asn1Decodable)
- Implement
doDecode(src)for type-specific decoding. - Optionally use/override
verifyTag(src, assertTag). - Use
decodeFromTlv(src, assertTag)for throwing decode. - Use
decodeFromTlvOrNull(src, assertTag)for non-throwing decode. - Use
decodeFromDer(src, assertTag)/decodeFromDerOrNull(src, assertTag)for direct DER decoding.
Generic Primitive Decoder
Asn1Primitive.decode(assertTag: ULong, transform: (ByteArray) -> T): TAsn1Primitive.decode(assertTag: Asn1Element.Tag, transform: (ByteArray) -> T): TAsn1Primitive.decodeOrNull(tag: ULong, transform: (ByteArray) -> T): T?
Asn1Primitive Typed Decode Helpers
| Category | Functions |
|---|---|
| Boolean | decodeToBoolean, decodeToBooleanOrNull |
| Integer family | decodeToInt, decodeToIntOrNull, decodeToLong, decodeToLongOrNull, decodeToUInt, decodeToUIntOrNull, decodeToULong, decodeToULongOrNull, decodeToAsn1Integer, decodeToAsn1IntegerOrNull |
| REAL family | decodeToAsn1Real, decodeToAsn1RealOrNull, decodeToDouble, decodeToDoubleOrNull, decodeToFloat, decodeToFloatOrNull |
| Enum family | decodeToEnumOrdinal, decodeToEnumOrdinalOrNull, decodeToEnum, decodeToEnumOrNull |
| String family | asAsn1String, decodeToUtf8String, decodeToUniversalString, decodeToIa5String, decodeToBmpString, decodeToTeletextString, decodeToPrintableString, decodeToNumericString, decodeToVisibleString, decodeToGeneralString, decodeToGraphicString, decodeToUnrestrictedString, decodeToVideotexString, decodeToString, decodeToStringOrNull |
| Time | decodeToInstant, decodeToInstantOrNull |
| Bit string | asAsn1BitString |
| Null handling | readNull, readNullOrNull |
Content-Byte Decode Helpers
These APIs decode only ASN.1 primitive payload bytes (no tag/length):
Int.decodeFromAsn1ContentBytesLong.decodeFromAsn1ContentBytesUInt.decodeFromAsn1ContentBytesULong.decodeFromAsn1ContentBytesBoolean.decodeFromAsn1ContentBytesString.decodeFromAsn1ContentBytesAsn1Integer.decodeFromAsn1ContentBytesAsn1Real.decodeFromAsn1ContentBytesObjectIdentifier.decodeFromAsn1ContentBytesInstant.decodeUtcTimeFromAsn1ContentBytesInstant.decodeGeneralizedTimeFromAsn1ContentBytes
Example
Raw ASN.1 Encoding
Typical Pipeline
- Start from a rich type or Kotlin primitive.
- Produce
Asn1Element(encodeToTlvor low-level primitive builders). - Materialize DER bytes (
derEncodedorencodeToDer).
High-level Encoding Contract (Asn1Encodable)
- Implement
encodeToTlv(). - Use
encodeToTlvOrNull()for non-throwing behaviour. - Use
encodeToDer()/encodeToDerOrNull()for DER output. - Use
withImplicitTag(...)overloads on encodable types.
Asn1Element.derEncoded provides lazy DER encoding for all ASN.1 nodes.
Primitive-Level Encoding Helpers
encodeToAsn1Primitive() is provided for:
Boolean,Int,Long,UInt,ULongEnum<*>Asn1Integer,Asn1RealDouble,Float(OrNullvariants available)String(UTF-8 ASN.1 string)
Content-Byte Encoding Helpers
encodeToAsn1ContentBytes() is provided for:
Boolean,Int,Long,UInt,ULongEnum<*>Asn1IntegerAsn1Real(member function)
Specialized Encoding Helpers
ByteArray.encodeToAsn1OctetStringPrimitive()ByteArray.encodeToAsn1BitStringPrimitive()ByteArray.encodeToAsn1BitStringContentBytes()Instant.encodeToAsn1UtcTimePrimitive()Instant.encodeToAsn1GeneralizedTimePrimitive()
Tagging: EXPLICIT and IMPLICIT
- EXPLICIT tagging wraps one or more children in a constructed context-specific container.
- IMPLICIT tagging replaces an element tag (with explicit control over class/constructed template).
Useful helpers:
Asn1.ExplicitlyTagged(tagNumber) { ... }Asn1.ExplicitTag(tagNumber)Asn1.ImplicitTag(tagNumber, tagClass)element withImplicitTag (...)tagNumber withClass TagClass.*tagNumber without CONSTRUCTED
ASN.1 Builder DSL Reference
Main DSL constructors under Asn1:
- Structures:
Sequence,SequenceOrNull,SequenceSafe,Set,SetOrNull,SetSafe,SetOf,SetOfOrNull,SetOfSafe,ExplicitlyTagged,ExplicitlyTaggedOrNull,OctetStringEncapsulating - Primitive builders:
Bool,Int(all integer overloads),Real(float/double),Enumerated,OctetString,BitString,Utf8String,PrintableString,Null,UtcTime,GeneralizedTime
Basic DSL
val frame = Asn1.Sequence {
+Asn1.Int(7)
+Asn1.Bool(true)
+Asn1.UtcTime(kotlin.time.Instant.parse("2026-01-01T12:30:45Z"))
}
val derHex = frame.derEncoded.toHexString()
derHex shouldBe /* (1)! */"30150201070101ff170d3236303130313132333034355a"
- Explore on asn1js.eu
Expanded Tagging
val tagged = Asn1.Sequence {
+Asn1.ExplicitlyTagged(1u) {
+Asn1.Bool(false)
}
+(Asn1.Utf8String("Foo") withImplicitTag (0xCAFEuL withClass TagClass.PRIVATE))
+(Asn1.Sequence { +Asn1.Int(42) } withImplicitTag (0x5EuL without CONSTRUCTED))
}
tagged.derEncoded.toHexString() shouldBe /* (1)! */"3013a103010100df83957e03466f6f9f5e0302012a"
- Explore on asn1js.eu
Robustness, Limits, and Hostile Input
awesn1's raw parser, encoder, and renderers are designed to fail predictably on adversarial input: a malformed or
deliberately oversized/deeply-nested DER blob yields a bounded Asn1Exception or a bounded result — never a
StackOverflowError, runaway recursion, or silent corruption.
The full robustness model (iterative parsing/encoding/rendering, Int/Long length guards, bounded
depth and collection counts, catchable-errors-only, the fuzzing/audit coverage behind it, the caller's
input-bounding responsibility, and the residual kotlinx.serialization footguns) is presented in Hardening, Fuzzing & Robustness.
Performance
awesn1's raw TLV layer aims for predictable, allocation-light parsing and encoding while staying fully iterative and
multiplatform. The JMH microbenchmarks below pit the raw Asn1Element layer against Bouncy Castle's ASN1Primitive,
and measure the cost of the length walk, string rendering, and SET sorting.
Benchmark environment
JMH 1.37, average-time mode (lower is better), 1 thread, 3×10 s warmup + 5×10 s measurement, single fork, JDK 17
(Corretto 17.0.10). MacBook Pro (Apple M3, 12 cores: 6 performance + 6 efficiency), macOS 26.5.1, on AC power.
These are microbenchmark figures — indicative, not contractual; re-run ./gradlew :benchmarks:jmh on your own
hardware. Bouncy Castle is a mature, hand-tuned baseline; awesn1 trades a little raw throughput for a fully
iterative, hardened, multiplatform implementation.
Fixtures: cert = a real self-signed X.509 v3 certificate; integers = a SEQUENCE of 50 INTEGERs;
mixed = a small SEQUENCE { INTEGER, OCTET STRING(32), BOOLEAN }.
Raw TLV Decode / Encode vs Bouncy Castle
| Operation (µs/op) | cert | integers | mixed |
|---|---|---|---|
| awesn1 decode | 3.513 ±0.04 | 4.884 ±0.34 | 1.172 ±0.01 |
| Bouncy Castle decode | 2.016 ±0.02 | 1.072 ±0.03 | 0.085 ±0.00 |
| awesn1 encode (warm, recomputes each call) | 0.830 ±0.00 | 1.065 ±0.01 | 0.103 ±0.00 |
| Bouncy Castle encode | 1.283 ±0.05 | 0.498 ±0.05 | 0.056 ±0.00 |
| awesn1 round-trip (cold: parse + encode) | 6.393 ±0.09 | 5.339 ±0.04 | 1.339 ±0.01 |
awesn1 derEncoded access (recomputes) |
0.800 ±0.00 | 0.819 ±0.00 | 0.064 ±0.00 |
The raw parser decodes in the low-single-digit-microsecond range — roughly 1.5–2× Bouncy Castle on the realistic
certificate fixture (more on the tiny integer/mixed fixtures, where fixed per-element overhead dominates a sub-microsecond
absolute cost). Warm stream-encoding is competitive with, and on the certificate faster than, BC. Note that, by design,
structures do not retain their encoding (Re-encoding is deliberate): repeated derEncoded access
recomputes O(size) each time, which the derEncoded access row quantifies as the accepted cost of stack-safe,
copy-free structures.
In addition, awesn1 always decodes any OCTET STRING's content bytes that are valid DER-encoded ASN.1 data, as this pattern
is so common in practice. This incurs overhead, but allows for asserting encapsulating OCTET STRINGs containing valid
ASN.1 data during parsing.
Length Walk, Rendering, SET Sorting
| Operation (µs/op) | cert | integers | mixed |
|---|---|---|---|
parse only |
3.723 ±0.36 | 5.562 ±0.14 | 1.209 ±0.02 |
parse + overallLengthLong (cold length walk) |
4.602 ±0.08 | 4.263 ±0.06 | 1.223 ±0.01 |
toString() (compact) |
18.028 ±0.12 | 8.673 ±0.21 | 0.593 ±0.01 |
prettyPrint() |
20.320 ±0.18 | 8.576 ±0.10 | 0.657 ±0.01 |
The content-length walk is a stack-safe post-order pass; parseThenLength − parseOnly puts it well under a microsecond
on the cert and mixed fixtures (the two integer figures overlap within run-to-run noise). Rendering is uncached and
bounded (see Hardening → bounded rendering).
Real-World Corpus Sweep vs Bouncy Castle
One invocation sweeps the entire real-world DER/PEM corpus shipped in crypto/src/jvmTest/resources (certificate
fixtures, attestation chains, real TLS certificates) once; both libraries pay an identical runCatching wrapper so the
comparison stays fair.
| Operation (µs/op, full sweep) | Score |
|---|---|
| awesn1 decode | 4745.460 ± 31.26 |
| Bouncy Castle decode | 2824.754 ± 25.46 |
| awesn1 encode | 1437.397 ± 20.11 |
| Bouncy Castle encode | 1227.419 ± 18.30 |
Memory
A parsed ASN.1 value is an in-memory object graph, so it occupies several times the DER bytes it was decoded from. That
amplification factor — not the wire size — is what to keep in mind when sizing untrusted input
(see Hardening → input bounding). The figures below hold
the parsed representation of the whole real-world certificate/attestation corpus
(crypto/src/jvmTest/resources, 690 DER blobs ≈ 0.63 MiB) and compare the retained heap across three forms.
| Held representation | parsed | retained heap | vs raw DER |
|---|---|---|---|
awesn1 raw Asn1Element tree |
690 | ~15.0 MiB | ~23.8× |
awesn1 typed X509Certificate (kxs) |
664 | ~5.9 MiB | ~9.6× |
Bouncy Castle X509Certificate (JCA) |
652 | ~3.3 MiB | ~5.6× |
The generic Asn1Element tree is the heaviest representation — it keeps a node wrapper, a tag, and a child container
per TLV element, which is the most flexible but least compact form. awesn1's typed kxs model collapses that
into purpose-built data classes (~2.5× leaner than the raw tree) and lands within ~2× of Bouncy Castle's hand-written
X.509 model. Note that the peak memory consumption while parsing will be the sum of the raw tree's memory consumption
and the typed X509Certificate model's.
Once the final certificate is constructed, the raw tree is free for garbage collection.
Method & caveats
Each form is the retained heap (used heap after System.gc(),
cross-checked with VisualVM heap dumps) holding only the parsed objects — the raw input bytes are dropped before
measuring. awesn1's typed model and Bouncy Castle accept slightly different cert subsets (664 vs 652), so each ratio
uses its own parsed-byte denominator. Figures are indicative (GC/JIT/JVM-version sensitive). Reproduce with the
NestingMemory probe under core/src/jvmTest.
Debugging and Inspection
Asn1Element.prettyPrint()andAsn1Encodable.prettyPrintAsn1()provide verbose human-readable trees. Output is length-bounded and truncated for very large/deep trees (see above).Asn1Element.toDerHexString()renders DER as hex.- OID pretty printing can include names from
KnownOIDsmappings.
See Also
- Serialization Tutorial: DER format via
kotlinx.serialization.