Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/run-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ jobs:
needs: [ build-project ]
uses: ./.github/workflows/run-tests-default.yml

asn1-tests:
needs: [ build-project ]
uses: ./.github/workflows/run-tests-asn1.yml

compatibility-tests:
needs: [ default-tests ]
needs: [ default-tests, asn1-tests ]
uses: ./.github/workflows/run-tests-compatibility.yml

slow-tasks:
Expand Down
30 changes: 30 additions & 0 deletions .github/workflows/run-tests-asn1.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Run ASN.1 tests
on:
workflow_dispatch:
workflow_call:

defaults:
run:
shell: bash

jobs:
asn1-tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
task:
- ':cryptography-serialization-asn1:jvmTest'
- ':cryptography-serialization-asn1:jsTest'
- ':cryptography-serialization-asn1:wasmJsNodeTest'
- ':cryptography-serialization-asn1:linuxX64Test'
- ':cryptography-serialization-asn1-modules:jvmTest'
- ':cryptography-serialization-asn1-modules:jsTest'
- ':cryptography-serialization-asn1-modules:wasmJsNodeTest'
- ':cryptography-serialization-asn1-modules:linuxX64Test'
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-environment
- name: Run ${{ matrix.task }}
run: ./gradlew -q ${{ matrix.task }} --continue

21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# CHANGELOG

## Unreleased

### ASN.1/DER

- RFC 8410 compliance: Ed25519/Ed448/X25519/X448 AlgorithmIdentifier encodes with ABSENT parameters; decoder tolerates explicit NULL.
- Unknown AlgorithmIdentifier parameters are preserved as raw ASN.1 for round-trip via new `Asn1Any` type.
- Support SEQUENCE OF (list) encode/decode in DER codec.

#### Breaking changes

- `UnknownKeyAlgorithmIdentifier` now carries `parameters: Any?` (previously always `null`) and the constructor signature changed to
`UnknownKeyAlgorithmIdentifier(algorithm: ObjectIdentifier, parameters: Any? = null)`. This is an ABI change in
`cryptography-serialization-asn1-modules`. Source usage that previously constructed the class with a single
`algorithm` argument remains valid.

#### Other improvements

- AlgorithmIdentifier base deserializer now permits an ABSENT-parameters path (subclass constructs the value without consuming
a parameters element), improving compatibility with inputs that omit parameters.


## 0.5.0 – CryptoKit & optimal providers

> Published 30 Jun 2025
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,22 @@ public final class dev/whyoleg/cryptography/serialization/asn1/ObjectIdentifier$
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class dev/whyoleg/cryptography/serialization/asn1/Asn1Any {
public static final field Companion Ldev/whyoleg/cryptography/serialization/asn1/Asn1Any$Companion;
public fun <init> ([B)V
public final fun getBytes ()[B
}

public final synthetic class dev/whyoleg/cryptography/serialization/asn1/Asn1Any$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Ldev/whyoleg/cryptography/serialization/asn1/Asn1Any$$serializer;
public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ldev/whyoleg/cryptography/serialization/asn1/Asn1Any;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Ldev/whyoleg/cryptography/serialization/asn1/Asn1Any;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
}

public final class dev/whyoleg/cryptography/serialization/asn1/Asn1Any$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,26 @@ final class dev.whyoleg.cryptography.serialization.asn1/BitArray { // dev.whyole
}
}

final class dev.whyoleg.cryptography.serialization.asn1/Asn1Any { // dev.whyoleg.cryptography.serialization.asn1/Asn1Any|null[0]
constructor <init>(kotlin/ByteArray) // dev.whyoleg.cryptography.serialization.asn1/Asn1Any.<init>|<init>(kotlin.ByteArray){}[0]

final val bytes // dev.whyoleg.cryptography.serialization.asn1/Asn1Any.bytes|{}bytes[0]
final fun <get-bytes>(): kotlin/ByteArray // dev.whyoleg.cryptography.serialization.asn1/Asn1Any.bytes.<get-bytes>|<get-bytes>(){}[0]

final object $serializer : kotlinx.serialization.internal/GeneratedSerializer<dev.whyoleg.cryptography.serialization.asn1/Asn1Any> { // dev.whyoleg.cryptography.serialization.asn1/Asn1Any.$serializer|null[0]
final val descriptor // dev.whyoleg.cryptography.serialization.asn1/Asn1Any.$serializer.descriptor|{}descriptor[0]
final fun <get-descriptor>(): kotlinx.serialization.descriptors/SerialDescriptor // dev.whyoleg.cryptography.serialization.asn1/Asn1Any.$serializer.descriptor.<get-descriptor>|<get-descriptor>(){}[0]

final fun childSerializers(): kotlin/Array<kotlinx.serialization/KSerializer<*>> // dev.whyoleg.cryptography.serialization.asn1/Asn1Any.$serializer.childSerializers|childSerializers(){}[0]
final fun deserialize(kotlinx.serialization.encoding/Decoder): dev.whyoleg.cryptography.serialization.asn1/Asn1Any // dev.whyoleg.cryptography.serialization.asn1/Asn1Any.$serializer.deserialize|deserialize(kotlinx.serialization.encoding.Decoder){}[0]
final fun serialize(kotlinx.serialization.encoding/Encoder, dev.whyoleg.cryptography.serialization.asn1/Asn1Any) // dev.whyoleg.cryptography.serialization.asn1/Asn1Any.$serializer.serialize|serialize(kotlinx.serialization.encoding.Encoder;dev.whyoleg.cryptography.serialization.asn1.Asn1Any){}[0]
}

final object Companion { // dev.whyoleg.cryptography.serialization.asn1/Asn1Any.Companion|null[0]
final fun serializer(): kotlinx.serialization/KSerializer<dev.whyoleg.cryptography.serialization.asn1/Asn1Any> // dev.whyoleg.cryptography.serialization.asn1/Asn1Any.Companion.serializer|serializer(){}[0]
}


final value class dev.whyoleg.cryptography.serialization.asn1/ObjectIdentifier { // dev.whyoleg.cryptography.serialization.asn1/ObjectIdentifier|null[0]
constructor <init>(kotlin/String) // dev.whyoleg.cryptography.serialization.asn1/ObjectIdentifier.<init>|<init>(kotlin.String){}[0]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,5 @@ public final class dev/whyoleg/cryptography/serialization/asn1/modules/UnknownKe
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun getAlgorithm-STa95mE ()Ljava/lang/String;
public synthetic fun getParameters ()Ljava/lang/Object;
public fun getParameters ()Ljava/lang/Void;
public fun getParameters ()Ljava/lang/Object;
}

Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,12 @@ final class dev.whyoleg.cryptography.serialization.asn1.modules/SubjectPublicKey
}

final class dev.whyoleg.cryptography.serialization.asn1.modules/UnknownKeyAlgorithmIdentifier : dev.whyoleg.cryptography.serialization.asn1.modules/KeyAlgorithmIdentifier { // dev.whyoleg.cryptography.serialization.asn1.modules/UnknownKeyAlgorithmIdentifier|null[0]
constructor <init>(dev.whyoleg.cryptography.serialization.asn1/ObjectIdentifier) // dev.whyoleg.cryptography.serialization.asn1.modules/UnknownKeyAlgorithmIdentifier.<init>|<init>(dev.whyoleg.cryptography.serialization.asn1.ObjectIdentifier){}[0]
constructor <init>(dev.whyoleg.cryptography.serialization.asn1/ObjectIdentifier, kotlin/Any?) // dev.whyoleg.cryptography.serialization.asn1.modules/UnknownKeyAlgorithmIdentifier.<init>|<init>(dev.whyoleg.cryptography.serialization.asn1.ObjectIdentifier;kotlin.Any?){}[0]

final val algorithm // dev.whyoleg.cryptography.serialization.asn1.modules/UnknownKeyAlgorithmIdentifier.algorithm|{}algorithm[0]
final fun <get-algorithm>(): dev.whyoleg.cryptography.serialization.asn1/ObjectIdentifier // dev.whyoleg.cryptography.serialization.asn1.modules/UnknownKeyAlgorithmIdentifier.algorithm.<get-algorithm>|<get-algorithm>(){}[0]
final val parameters // dev.whyoleg.cryptography.serialization.asn1.modules/UnknownKeyAlgorithmIdentifier.parameters|{}parameters[0]
final fun <get-parameters>(): kotlin/Nothing? // dev.whyoleg.cryptography.serialization.asn1.modules/UnknownKeyAlgorithmIdentifier.parameters.<get-parameters>|<get-parameters>(){}[0]
final fun <get-parameters>(): kotlin/Any? // dev.whyoleg.cryptography.serialization.asn1.modules/UnknownKeyAlgorithmIdentifier.parameters.<get-parameters>|<get-parameters>(){}[0]
}

final value class dev.whyoleg.cryptography.serialization.asn1.modules/EcParameters { // dev.whyoleg.cryptography.serialization.asn1.modules/EcParameters|null[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,17 @@ public abstract class AlgorithmIdentifierSerializer<AI : AlgorithmIdentifier> :
index = 0,
deserializer = ObjectIdentifier.serializer()
)
check(decodeElementIndex(descriptor) == 1)
val parameters = decodeParameters(algorithm)
check(decodeElementIndex(descriptor) == CompositeDecoder.DECODE_DONE)
parameters
when (val idx = decodeElementIndex(descriptor)) {
1 -> {
val parameters = decodeParameters(algorithm)
check(decodeElementIndex(descriptor) == CompositeDecoder.DECODE_DONE)
parameters
}
CompositeDecoder.DECODE_DONE -> {
// Some algorithms may omit parameters. Delegate to subclass without consuming parameters.
decodeParameters(algorithm)
}
else -> error("Unexpected element index: $idx")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import kotlinx.serialization.*
@Serializable(KeyAlgorithmIdentifierSerializer::class)
public interface KeyAlgorithmIdentifier : AlgorithmIdentifier

public class UnknownKeyAlgorithmIdentifier(override val algorithm: ObjectIdentifier) : KeyAlgorithmIdentifier {
override val parameters: Nothing? get() = null
}

public class UnknownKeyAlgorithmIdentifier(
override val algorithm: ObjectIdentifier,
override val parameters: Any? = null,
) : KeyAlgorithmIdentifier
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,31 @@ import kotlinx.serialization.encoding.*

@OptIn(ExperimentalSerializationApi::class)
internal object KeyAlgorithmIdentifierSerializer : AlgorithmIdentifierSerializer<KeyAlgorithmIdentifier>() {
override fun CompositeEncoder.encodeParameters(value: KeyAlgorithmIdentifier): Unit = when (value) {
is RsaKeyAlgorithmIdentifier -> encodeParameters(NothingSerializer(), RsaKeyAlgorithmIdentifier.parameters)
is EcKeyAlgorithmIdentifier -> encodeParameters(EcParameters.serializer(), value.parameters)
is UnknownKeyAlgorithmIdentifier -> encodeParameters(NothingSerializer(), value.parameters)
else -> encodeParameters(NothingSerializer(), null)
override fun CompositeEncoder.encodeParameters(value: KeyAlgorithmIdentifier) {
when (value) {
is RsaKeyAlgorithmIdentifier -> encodeParameters(NothingSerializer(), null) // explicit NULL per RSA
is EcKeyAlgorithmIdentifier -> encodeParameters(EcParameters.serializer(), value.parameters)
is UnknownKeyAlgorithmIdentifier -> {
// RFC 8410: parameters MUST be ABSENT for Ed25519/Ed448/X25519/X448
if (value.algorithm.isRfc8410NoParams()) return
when (val p = value.parameters) {
null -> {
// For unknown algorithms, prefer ABSENT when no parameters provided
// (do nothing). If explicit NULL must be preserved, p will be Asn1Any(05 00).
return
}
is Asn1Any -> encodeParameters(Asn1Any.serializer(), p)
else -> {
// Fallback: encode NULL to avoid guessing structure
encodeParameters(NothingSerializer(), null)
}
}
}
else -> {
// Safe default for other known types if any
encodeParameters(NothingSerializer(), null)
}
}
}

override fun CompositeDecoder.decodeParameters(algorithm: ObjectIdentifier): KeyAlgorithmIdentifier = when (algorithm) {
Expand All @@ -26,8 +46,14 @@ internal object KeyAlgorithmIdentifierSerializer : AlgorithmIdentifierSerializer
}
ObjectIdentifier.EC -> EcKeyAlgorithmIdentifier(decodeParameters(EcParameters.serializer()))
else -> {
// TODO: somehow we should ignore parameters here
UnknownKeyAlgorithmIdentifier(algorithm)
// Capture unknown parameters as raw ASN.1 for round-trip when present; null means ABSENT
val raw: Asn1Any? = try {
decodeParameters(Asn1Any.serializer())
} catch (_: IllegalStateException) {
// No element to read (ABSENT)
null
}
UnknownKeyAlgorithmIdentifier(algorithm, raw)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.whyoleg.cryptography.serialization.asn1.modules

import dev.whyoleg.cryptography.serialization.asn1.ObjectIdentifier

public val ObjectIdentifier.Companion.Ed25519: ObjectIdentifier get() = ObjectIdentifier("1.3.101.112")
public val ObjectIdentifier.Companion.Ed448: ObjectIdentifier get() = ObjectIdentifier("1.3.101.113")

public val ObjectIdentifier.Companion.X25519: ObjectIdentifier get() = ObjectIdentifier("1.3.101.110")
public val ObjectIdentifier.Companion.X448: ObjectIdentifier get() = ObjectIdentifier("1.3.101.111")

internal fun ObjectIdentifier.isRfc8410NoParams(): Boolean =
this == ObjectIdentifier.Ed25519 ||
this == ObjectIdentifier.Ed448 ||
this == ObjectIdentifier.X25519 ||
this == ObjectIdentifier.X448
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.whyoleg.cryptography.serialization.asn1.modules

import dev.whyoleg.cryptography.serialization.asn1.*
import kotlinx.serialization.decodeFromByteArray
import kotlin.test.*

class KeyAlgorithmIdentifierDecodeTest {

private fun String.hexToBytes(): ByteArray {
check(length % 2 == 0) { "Invalid hex length" }
return ByteArray(length / 2) { i ->
val hi = this[i * 2].digitToInt(16)
val lo = this[i * 2 + 1].digitToInt(16)
((hi shl 4) or lo).toByte()
}
}

@Test
fun decode_Ed25519_absentParameters() {
// SEQUENCE { algorithm OBJECT IDENTIFIER 1.3.101.112 } (no parameters element)
val bytes = "300506032B6570".hexToBytes()
val id = Der.decodeFromByteArray<KeyAlgorithmIdentifier>(bytes)
assertTrue(id is UnknownKeyAlgorithmIdentifier)
assertEquals(ObjectIdentifier.Ed25519, id.algorithm)
}

@Test
fun decode_Ed25519_nullParameters() {
// SEQUENCE { algorithm OBJECT IDENTIFIER 1.3.101.112, parameters NULL }
val bytes = "300706032B65700500".hexToBytes()
val id = Der.decodeFromByteArray<KeyAlgorithmIdentifier>(bytes)
assertTrue(id is UnknownKeyAlgorithmIdentifier)
assertEquals(ObjectIdentifier.Ed25519, id.algorithm)
}

@Test
fun decode_X25519_absentParameters() {
// SEQUENCE { algorithm OBJECT IDENTIFIER 1.3.101.110 } (no parameters element)
val bytes = "300506032B656E".hexToBytes()
val id = Der.decodeFromByteArray<KeyAlgorithmIdentifier>(bytes)
assertTrue(id is UnknownKeyAlgorithmIdentifier)
assertEquals(ObjectIdentifier.X25519, id.algorithm)
}

@Test
fun decode_X25519_nullParameters() {
// SEQUENCE { algorithm OBJECT IDENTIFIER 1.3.101.110, parameters NULL }
val bytes = "300706032B656E0500".hexToBytes()
val id = Der.decodeFromByteArray<KeyAlgorithmIdentifier>(bytes)
assertTrue(id is UnknownKeyAlgorithmIdentifier)
assertEquals(ObjectIdentifier.X25519, id.algorithm)
}

@Test
fun decode_Ed448_absentParameters() {
val bytes = "300506032B6571".hexToBytes()
val id = Der.decodeFromByteArray<KeyAlgorithmIdentifier>(bytes)
assertTrue(id is UnknownKeyAlgorithmIdentifier)
assertEquals(ObjectIdentifier.Ed448, id.algorithm)
}

@Test
fun decode_Ed448_nullParameters() {
val bytes = "300706032B65710500".hexToBytes()
val id = Der.decodeFromByteArray<KeyAlgorithmIdentifier>(bytes)
assertTrue(id is UnknownKeyAlgorithmIdentifier)
assertEquals(ObjectIdentifier.Ed448, id.algorithm)
}

@Test
fun decode_X448_absentParameters() {
val bytes = "300506032B6570".replace("70","6F").hexToBytes() // 1.3.101.111
val id = Der.decodeFromByteArray<KeyAlgorithmIdentifier>(bytes)
assertTrue(id is UnknownKeyAlgorithmIdentifier)
assertEquals(ObjectIdentifier.X448, id.algorithm)
}

@Test
fun decode_X448_nullParameters() {
val bytes = "300706032B656F0500".hexToBytes()
val id = Der.decodeFromByteArray<KeyAlgorithmIdentifier>(bytes)
assertTrue(id is UnknownKeyAlgorithmIdentifier)
assertEquals(ObjectIdentifier.X448, id.algorithm)
}
}
Loading