Skip to content

Commit e1d79f1

Browse files
committed
asn1(core): RFC 8410 OIDs + ABSENT params; preserve unknown AlgorithmIdentifier params via Asn1Any; add SEQUENCE OF support; update API/ABI and changelog
- Add ObjectIdentifier extensions for Ed25519/Ed448/X25519/X448 - Enforce RFC 8410 on encode (omit parameters); decoder tolerates NULL - Introduce Asn1Any and carry parameters in UnknownKeyAlgorithmIdentifier - Support StructureKind.LIST (SEQUENCE OF) in DER codec - Update API dumps and CHANGELOG; add PR description
1 parent 1d0f98f commit e1d79f1

File tree

13 files changed

+190
-46
lines changed

13 files changed

+190
-46
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# CHANGELOG
22

3+
## Unreleased
4+
5+
### ASN.1/DER
6+
7+
- RFC 8410 compliance: Ed25519/Ed448/X25519/X448 AlgorithmIdentifier encodes with ABSENT parameters; decoder tolerates explicit NULL.
8+
- Unknown AlgorithmIdentifier parameters are preserved as raw ASN.1 for round-trip via new `Asn1Any` type.
9+
- Support SEQUENCE OF (list) encode/decode in DER codec.
10+
11+
312
## 0.5.0 – CryptoKit & optimal providers
413

514
> Published 30 Jun 2025

cryptography-serialization/asn1/api/cryptography-serialization-asn1.api

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,22 @@ public final class dev/whyoleg/cryptography/serialization/asn1/ObjectIdentifier$
9090
public final fun serializer ()Lkotlinx/serialization/KSerializer;
9191
}
9292

93+
public final class dev/whyoleg/cryptography/serialization/asn1/Asn1Any {
94+
public static final field Companion Ldev/whyoleg/cryptography/serialization/asn1/Asn1Any$Companion;
95+
public fun <init> ([B)V
96+
public final fun getBytes ()[B
97+
}
98+
99+
public final synthetic class dev/whyoleg/cryptography/serialization/asn1/Asn1Any$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
100+
public static final field INSTANCE Ldev/whyoleg/cryptography/serialization/asn1/Asn1Any$$serializer;
101+
public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
102+
public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ldev/whyoleg/cryptography/serialization/asn1/Asn1Any;
103+
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
104+
public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
105+
public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Ldev/whyoleg/cryptography/serialization/asn1/Asn1Any;)V
106+
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
107+
}
108+
109+
public final class dev/whyoleg/cryptography/serialization/asn1/Asn1Any$Companion {
110+
public final fun serializer ()Lkotlinx/serialization/KSerializer;
111+
}

cryptography-serialization/asn1/api/cryptography-serialization-asn1.klib.api

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,26 @@ final class dev.whyoleg.cryptography.serialization.asn1/BitArray { // dev.whyole
4848
}
4949
}
5050

51+
final class dev.whyoleg.cryptography.serialization.asn1/Asn1Any { // dev.whyoleg.cryptography.serialization.asn1/Asn1Any|null[0]
52+
constructor <init>(kotlin/ByteArray) // dev.whyoleg.cryptography.serialization.asn1/Asn1Any.<init>|<init>(kotlin.ByteArray){}[0]
53+
54+
final val bytes // dev.whyoleg.cryptography.serialization.asn1/Asn1Any.bytes|{}bytes[0]
55+
final fun <get-bytes>(): kotlin/ByteArray // dev.whyoleg.cryptography.serialization.asn1/Asn1Any.bytes.<get-bytes>|<get-bytes>(){}[0]
56+
57+
final object $serializer : kotlinx.serialization.internal/GeneratedSerializer<dev.whyoleg.cryptography.serialization.asn1/Asn1Any> { // dev.whyoleg.cryptography.serialization.asn1/Asn1Any.$serializer|null[0]
58+
final val descriptor // dev.whyoleg.cryptography.serialization.asn1/Asn1Any.$serializer.descriptor|{}descriptor[0]
59+
final fun <get-descriptor>(): kotlinx.serialization.descriptors/SerialDescriptor // dev.whyoleg.cryptography.serialization.asn1/Asn1Any.$serializer.descriptor.<get-descriptor>|<get-descriptor>(){}[0]
60+
61+
final fun childSerializers(): kotlin/Array<kotlinx.serialization/KSerializer<*>> // dev.whyoleg.cryptography.serialization.asn1/Asn1Any.$serializer.childSerializers|childSerializers(){}[0]
62+
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]
63+
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]
64+
}
65+
66+
final object Companion { // dev.whyoleg.cryptography.serialization.asn1/Asn1Any.Companion|null[0]
67+
final fun serializer(): kotlinx.serialization/KSerializer<dev.whyoleg.cryptography.serialization.asn1/Asn1Any> // dev.whyoleg.cryptography.serialization.asn1/Asn1Any.Companion.serializer|serializer(){}[0]
68+
}
69+
70+
5171
final value class dev.whyoleg.cryptography.serialization.asn1/ObjectIdentifier { // dev.whyoleg.cryptography.serialization.asn1/ObjectIdentifier|null[0]
5272
constructor <init>(kotlin/String) // dev.whyoleg.cryptography.serialization.asn1/ObjectIdentifier.<init>|<init>(kotlin.String){}[0]
5373

cryptography-serialization/asn1/modules/api/cryptography-serialization-asn1-modules.api

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,5 @@ public final class dev/whyoleg/cryptography/serialization/asn1/modules/UnknownKe
248248
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
249249
public fun getAlgorithm-STa95mE ()Ljava/lang/String;
250250
public synthetic fun getParameters ()Ljava/lang/Object;
251-
public fun getParameters ()Ljava/lang/Void;
251+
public fun getParameters ()Ljava/lang/Object;
252252
}
253-

cryptography-serialization/asn1/modules/api/cryptography-serialization-asn1-modules.klib.api

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,12 +200,12 @@ final class dev.whyoleg.cryptography.serialization.asn1.modules/SubjectPublicKey
200200
}
201201

202202
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]
203-
constructor <init>(dev.whyoleg.cryptography.serialization.asn1/ObjectIdentifier) // dev.whyoleg.cryptography.serialization.asn1.modules/UnknownKeyAlgorithmIdentifier.<init>|<init>(dev.whyoleg.cryptography.serialization.asn1.ObjectIdentifier){}[0]
203+
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]
204204

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

211211
final value class dev.whyoleg.cryptography.serialization.asn1.modules/EcParameters { // dev.whyoleg.cryptography.serialization.asn1.modules/EcParameters|null[0]

cryptography-serialization/asn1/modules/src/commonMain/kotlin/KeyAlgorithmIdentifier.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import kotlinx.serialization.*
1010
@Serializable(KeyAlgorithmIdentifierSerializer::class)
1111
public interface KeyAlgorithmIdentifier : AlgorithmIdentifier
1212

13-
public class UnknownKeyAlgorithmIdentifier(override val algorithm: ObjectIdentifier) : KeyAlgorithmIdentifier {
14-
override val parameters: Nothing? get() = null
15-
}
16-
13+
public class UnknownKeyAlgorithmIdentifier(
14+
override val algorithm: ObjectIdentifier,
15+
override val parameters: Any? = null,
16+
) : KeyAlgorithmIdentifier

cryptography-serialization/asn1/modules/src/commonMain/kotlin/KeyAlgorithmIdentifierSerializer.kt

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,31 @@ import kotlinx.serialization.encoding.*
1111

1212
@OptIn(ExperimentalSerializationApi::class)
1313
internal object KeyAlgorithmIdentifierSerializer : AlgorithmIdentifierSerializer<KeyAlgorithmIdentifier>() {
14-
override fun CompositeEncoder.encodeParameters(value: KeyAlgorithmIdentifier): Unit = when (value) {
15-
is RsaKeyAlgorithmIdentifier -> encodeParameters(NothingSerializer(), RsaKeyAlgorithmIdentifier.parameters)
16-
is EcKeyAlgorithmIdentifier -> encodeParameters(EcParameters.serializer(), value.parameters)
17-
is UnknownKeyAlgorithmIdentifier -> encodeParameters(NothingSerializer(), value.parameters)
18-
else -> encodeParameters(NothingSerializer(), null)
14+
override fun CompositeEncoder.encodeParameters(value: KeyAlgorithmIdentifier) {
15+
when (value) {
16+
is RsaKeyAlgorithmIdentifier -> encodeParameters(NothingSerializer(), null) // explicit NULL per RSA
17+
is EcKeyAlgorithmIdentifier -> encodeParameters(EcParameters.serializer(), value.parameters)
18+
is UnknownKeyAlgorithmIdentifier -> {
19+
// RFC 8410: parameters MUST be ABSENT for Ed25519/Ed448/X25519/X448
20+
if (value.algorithm.isRfc8410NoParams()) return
21+
when (val p = value.parameters) {
22+
null -> {
23+
// For unknown algorithms, prefer ABSENT when no parameters provided
24+
// (do nothing). If explicit NULL must be preserved, p will be Asn1Any(05 00).
25+
return
26+
}
27+
is Asn1Any -> encodeParameters(Asn1Any.serializer(), p)
28+
else -> {
29+
// Fallback: encode NULL to avoid guessing structure
30+
encodeParameters(NothingSerializer(), null)
31+
}
32+
}
33+
}
34+
else -> {
35+
// Safe default for other known types if any
36+
encodeParameters(NothingSerializer(), null)
37+
}
38+
}
1939
}
2040

2141
override fun CompositeDecoder.decodeParameters(algorithm: ObjectIdentifier): KeyAlgorithmIdentifier = when (algorithm) {
@@ -26,8 +46,14 @@ internal object KeyAlgorithmIdentifierSerializer : AlgorithmIdentifierSerializer
2646
}
2747
ObjectIdentifier.EC -> EcKeyAlgorithmIdentifier(decodeParameters(EcParameters.serializer()))
2848
else -> {
29-
// TODO: somehow we should ignore parameters here
30-
UnknownKeyAlgorithmIdentifier(algorithm)
49+
// Capture unknown parameters as raw ASN.1 for round-trip when present; null means ABSENT
50+
val raw: Asn1Any? = try {
51+
decodeParameters(Asn1Any.serializer())
52+
} catch (_: IllegalStateException) {
53+
// No element to read (ABSENT)
54+
null
55+
}
56+
UnknownKeyAlgorithmIdentifier(algorithm, raw)
3157
}
3258
}
33-
}
59+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package dev.whyoleg.cryptography.serialization.asn1.modules
6+
7+
import dev.whyoleg.cryptography.serialization.asn1.ObjectIdentifier
8+
9+
public val ObjectIdentifier.Companion.Ed25519: ObjectIdentifier get() = ObjectIdentifier("1.3.101.112")
10+
public val ObjectIdentifier.Companion.Ed448: ObjectIdentifier get() = ObjectIdentifier("1.3.101.113")
11+
12+
public val ObjectIdentifier.Companion.X25519: ObjectIdentifier get() = ObjectIdentifier("1.3.101.110")
13+
public val ObjectIdentifier.Companion.X448: ObjectIdentifier get() = ObjectIdentifier("1.3.101.111")
14+
15+
internal fun ObjectIdentifier.isRfc8410NoParams(): Boolean =
16+
this == ObjectIdentifier.Ed25519 ||
17+
this == ObjectIdentifier.Ed448 ||
18+
this == ObjectIdentifier.X25519 ||
19+
this == ObjectIdentifier.X448
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package dev.whyoleg.cryptography.serialization.asn1
6+
7+
import kotlinx.serialization.Serializable
8+
9+
/**
10+
* Represents a raw ASN.1 element (tag + length + value) captured as-is.
11+
* Useful for preserving unknown parameters for round-trip encoding.
12+
*/
13+
@Serializable
14+
public class Asn1Any(public val bytes: ByteArray)
15+

cryptography-serialization/asn1/src/commonMain/kotlin/internal/DerDecoder.kt

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -29,32 +29,7 @@ internal class DerDecoder(
2929
return tag
3030
}
3131

32-
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
33-
if (input.eof) return CompositeDecoder.DECODE_DONE
34-
35-
val tag = input.peakTag()
36-
37-
while (true) {
38-
val index = currentIndex
39-
tagOverride = descriptor.getElementContextSpecificTag(index)
40-
41-
if (descriptor.isElementOptional(index)) {
42-
val requiredTag = checkNotNull(tagOverride) {
43-
"Optional element $descriptor[$index] must have context specific tag"
44-
}
45-
46-
// if the tag is different,
47-
// then an optional element is absent,
48-
// and so we need to increment the index
49-
if (tag != requiredTag.tag) {
50-
currentIndex++
51-
continue
52-
}
53-
}
54-
55-
return currentIndex++
56-
}
57-
}
32+
5833

5934
override fun decodeNotNullMark(): Boolean = input.isNotNull()
6035
override fun decodeNull(): Nothing? = input.readNull()
@@ -69,18 +44,47 @@ internal class DerDecoder(
6944
BitArray.serializer().descriptor -> input.readBitString(getAndResetTagOverride()) as T
7045
ObjectIdentifier.serializer().descriptor -> input.readObjectIdentifier(getAndResetTagOverride()) as T
7146
BigInt.serializer().descriptor -> input.readInteger(getAndResetTagOverride()) as T
47+
Asn1Any.serializer().descriptor -> Asn1Any(input.readAnyElement(getAndResetTagOverride())) as T
7248
else -> deserializer.deserialize(this)
7349
}
7450

7551
// structures: SEQUENCE and SEQUENCE OF
7652
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder = when (descriptor.kind) {
77-
StructureKind.CLASS, is PolymorphicKind -> DerDecoder(der, input.readSequence(getAndResetTagOverride()))
78-
else -> throw SerializationException("This serial kind is not supported as structure: $descriptor")
53+
StructureKind.CLASS, is PolymorphicKind, StructureKind.LIST -> DerDecoder(der, input.readSequence(getAndResetTagOverride()))
54+
else -> throw SerializationException("This serial kind is not supported as structure: $descriptor")
7955
}
8056

8157
override fun decodeInline(descriptor: SerialDescriptor): Decoder = this
8258
override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int): Decoder = this
8359

60+
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
61+
if (descriptor.kind == StructureKind.LIST) {
62+
return if (input.eof) CompositeDecoder.DECODE_DONE else currentIndex++
63+
}
64+
if (input.eof) return CompositeDecoder.DECODE_DONE
65+
66+
val tag = input.peakTag()
67+
68+
while (true) {
69+
val index = currentIndex
70+
if (index >= descriptor.elementsCount) return CompositeDecoder.DECODE_DONE
71+
tagOverride = descriptor.getElementContextSpecificTag(index)
72+
73+
if (descriptor.isElementOptional(index)) {
74+
val requiredTag = checkNotNull(tagOverride) {
75+
"Optional element $descriptor[$index] must have context specific tag"
76+
}
77+
78+
if (tag != requiredTag.tag) {
79+
currentIndex++
80+
continue
81+
}
82+
}
83+
84+
return currentIndex++
85+
}
86+
}
87+
8488
// could be supported, but later when it will be needed
8589
override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = error("Enum decoding is not supported")
8690
override fun decodeString(): String = error("String decoding is not supported")

0 commit comments

Comments
 (0)