Skip to content
awesn1

A-SIT Plus Official GitHub licence Kotlin Multiplatform Kotlin 2.3.0 Java 17 Maven Central

Overview

Awesome Syntax Notation One (awesn1) /ɑː es en wʌn/ 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 kxs module (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

implementation("at.asitplus.awesn1:core:$version")

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.serialization format (provided by the kxs module).

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:

implementation("at.asitplus.awesn1:kxs:$version")

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
}
  1. Explore on asn1js.eu
  2. Explore on asn1js.eu

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"
  1. 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
  1. Explore on asn1js.eu

PEM Armor

core includes generic PEM support:

  • PemBlock / PemHeader data structures
  • decodeFromPem / encodeToPem for single blocks
  • decodeAllFromPem / encodeAllToPem for PEM documents with multiple blocks
  • independent PemEncodable / PemDecodable contracts
  • Asn1PemEncodable / Asn1PemDecodable bridge 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