Overview
Awesome Syntax Notation One (awesn1) makes ASN.1 a joy to work with; probably for the first time ever. It provides the most sophisticated Kotlin Multiplatform ASN.1 toolbox in the known universe. It gives you:
- First-class kotlinx.serialization support through a dedicated
kxsmodule (see Serialization).
Simply model Kotlin classes and never care for ASN.1 details again. No sleight of hand, no cheap tricks! - ASN.1 element types (
Asn1Element,Asn1Primitive,Asn1Structure) - DER parsing/encoding helpers
- A builder DSL for manual ASN.1 trees (
Asn1.Sequence,Asn1.Set,Asn1.ExplicitlyTagged, ...) - Rich ASN.1 domain types (
ObjectIdentifier,Asn1Integer,Asn1Real,Asn1Time,Asn1String,BitSet) - Addons for integrating with kotlinx-io (see io addons)
- Optional known OID registry (see OID addons)
Maven Coordinates
What About Certificates, Public Keys, and PKI Types?
Those are intentionally not part of core.
core contains generic ASN.1 infrastructure and rich built-in ASN.1 data types.
Cryptographic structures such as X.509 certificates, SubjectPublicKeyInfo, PrivateKeyInfo, PKCS#10 requests,
and related PKI data classes live in the dedicated crypto module instead.
That split keeps core small and broadly reusable, while crypto builds on top of it with
cryptograph-specific data models.
Supply Chain Metadata
CycloneDX SBOMs for awesn1 are published with each Maven publication on Maven Central and exported on this documentation site. See SBOM for publication-specific JSON/XML downloads and the machine-readable index.
Package Map
at.asitplus.awesn1: Element model, rich ASN.1 types, tagging, parsing helpers.at.asitplus.awesn1.encoding: Builder DSL plus low-level encode/decode helpers.at.asitplus.awesn1.serialization:kotlinx.serializationformat (provided by thekxsmodule).
Serialization Example: RFC CHOICE with Sealed Polymorphism
kotlinx.serialization integration
Integration with kotlinx.serialization requires the kxs module. If you require kotlinx.serialization support
add the following dependency:
The kxs module provides the DER format implementation (DER.encodeToByteArray, DER.decodeFromByteArray, ...).
Core ASN.1 types such as Asn1Integer, Asn1Real, and ObjectIdentifier are serializable in a way that they can also be used
with non-DER formats.
This example models a subset of GeneralName ::= CHOICE from RFC 5280 (dNSName [2] IA5String,
uniformResourceIdentifier [6] IA5String), encodes two alternatives, round-trips decoding, and asserts exact DER hex
bytes.
Reference: RFC 5280, GeneralName.
@Serializable
private sealed interface Rfc5280GeneralName
@Serializable
@JvmInline
@Asn1Tag(
tagNumber = 2u,
tagClass = Asn1TagClass.CONTEXT_SPECIFIC,
constructed = Asn1ConstructedBit.PRIMITIVE,
)
private value class Rfc5280DnsName(
val value: String,
) : Rfc5280GeneralName
@Serializable
@JvmInline
@Asn1Tag(
tagNumber = 6u,
tagClass = Asn1TagClass.CONTEXT_SPECIFIC,
constructed = Asn1ConstructedBit.PRIMITIVE,
)
private value class Rfc5280UriName(
val value: String,
) : Rfc5280GeneralName
private fun coreHookSerializationChoiceRfcDer(): Pair<ByteArray, ByteArray> {
val dnsName: Rfc5280GeneralName = Rfc5280DnsName("example.com")
val uriName: Rfc5280GeneralName = Rfc5280UriName("https://example.com")
val dnsDer = DER.encodeToByteArray(dnsName)
dnsDer.toHexString() shouldBe "820b6578616d706c652e636f6d" /* (1)! */
DER.decodeFromByteArray<Rfc5280GeneralName>(dnsDer) shouldBe dnsName
val uriDer = DER.encodeToByteArray(uriName)
uriDer.toHexString() shouldBe "861368747470733a2f2f6578616d706c652e636f6d" /* (2)! */
DER.decodeFromByteArray<Rfc5280GeneralName>(uriDer) shouldBe uriName
return dnsDer to uriDer
}
For a complete envelope-style serialization walkthrough (raw payload preservation, implicit tagging, signature-verification use-case), see the Serialization Tutorial.
ASN.1 Builder DSL Showcase
awesn1 comes with a type-safe ASN.1 Builder 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
Example: Define Your Own Semantic Type
Low-Level ASN.1 modelling and processing is also possible.
To define custom types, implement Asn1Encodable and provide a companion Asn1Decodable when you want a semantic model
on top of raw TLV.
If you want it to be serializable in DER, make the companion also implement Asn1Serializable and reference it in the
@Serializable annotation on your type.
(Note that this works only for ASN.1 serialization, not in a generic fashion.)
private data class SemanticVersion(
val major: Int,
val minor: Int,
) : Asn1Encodable<Asn1Sequence> {
override fun encodeToTlv(): Asn1Sequence = Asn1.Sequence {
+Asn1.Int(major)
+Asn1.Int(minor)
}
companion object : Asn1Decodable<Asn1Sequence, SemanticVersion> {
override fun doDecode(src: Asn1Sequence): SemanticVersion = src.decodeRethrowing {
SemanticVersion(
major = next().asPrimitive().decodeToInt(),
minor = next().asPrimitive().decodeToInt(),
)
}
}
}
val version = SemanticVersion(major = 1, minor = 42)
val der = version.encodeToDer()
der.toHexString() shouldBe "300602010102012a" /* (1)! */
SemanticVersion.decodeFromDer(der) shouldBe version
- Explore on asn1js.eu
PEM Armor
core includes generic PEM support:
PemBlock/PemHeaderdata structuresdecodeFromPem/encodeToPemfor single blocksdecodeAllFromPem/encodeAllToPemfor PEM documents with multiple blocks- independent
PemEncodable/PemDecodablecontracts Asn1PemEncodable/Asn1PemDecodablebridge contracts for ASN.1 DER payloads
Generic PEM (opaque payload bytes)
Use this when you want to parse or emit PEM without caring what the payload is:
val source = """
-----BEGIN CERTIFICATE-----
AQID
-----END CERTIFICATE-----
-----BEGIN PUBLIC KEY-----
BAUG
-----END PUBLIC KEY-----
""".trimIndent()
val blocks: List<PemBlock> = decodeAllFromPem(source)
blocks.map { it.label } shouldBe listOf("CERTIFICATE", "PUBLIC KEY")
val encryptedLegacy = PemBlock(
label = "RSA PRIVATE KEY",
headers = listOf(
PemHeader("Proc-Type", "4,ENCRYPTED"),
PemHeader("DEK-Info", "AES-256-CBC,00112233445566778899AABBCCDDEEFF")
),
payload = byteArrayOf(1, 2, 3)
)
val pemText = encodeAllToPem(listOf(encryptedLegacy))
decodeFromPem(pemText).headers.map { it.name } shouldBe listOf("Proc-Type", "DEK-Info")
ASN.1 Payloads Inside PEM
If your PEM payload is ASN.1 DER, implement both ASN.1 and PEM bridge contracts:
val source = object : Asn1PemEncodable<Asn1Primitive> {
override val pemLabel: String = "ASN1 INTEGER"
override fun encodeToTlv(): Asn1Primitive = Asn1Integer(42).encodeToTlv()
}
val decoder = object : Asn1PemDecodable<Asn1Primitive, Asn1Integer>,
Asn1Decodable<Asn1Primitive, Asn1Integer> by Asn1Integer.Companion {}
val pem = source.encodeToPem()
decoder.decodeFromPem(pem) shouldBe Asn1Integer(42)
Background
awesn1 was originally called Indispensable ASN.1 and was one of Signum's pillars: a comprehensive Kotlin Multiplatform ASN.1 implementation. It'd design was shaped by production use and informed by opinionated design choices based real-world experience.
That original design came with trade-offs. To support Swift usage, it depended on KmmResult. At the same time, it focused narrowly on ASN.1’s format details, and its API was heavily tailored to Signum’s use cases.
With feedback from Oleg Yukhnevich, first-class kotlinx.serialization support was prioritised, and awesn1 was separated into an independent library. Third-party dependencies were removed, the API was simplified, and the core was streamlined. Integration with kotlinx-io is now optional. In the end, the already solid ASN.1 handling was barely touched, only delicately polished.
Whenever ASN.1 makes me sad, I stop being sad and be awesome instead. True story.
— awesn1 users