Skip to content

Commit 10f5f0f

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 10f5f0f

14 files changed

+267
-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

PR_asn1_oids_rfc8410.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
Title: ASN.1: RFC 8410 OIDs, ABSENT params, Unknown key preservation, SEQUENCE OF support
2+
3+
Summary
4+
- Add RFC 8410 OIDs as `ObjectIdentifier` extensions: Ed25519, Ed448, X25519, X448.
5+
- Make AlgorithmIdentifier RFC 8410–conformant on encode: parameters are ABSENT for the four OIDs; decoder tolerates explicit NULL.
6+
- Support unknown AlgorithmIdentifier keys robustly by preserving parameters as raw ASN.1 for round‑trip (`Asn1Any`).
7+
- Add SEQUENCE OF (list) support to the DER codec.
8+
- Extend unit tests (encode/decode/round‑trip, SPKI vectors, negative cases) and wire focused multi‑platform CI runs for ASN.1 modules.
9+
10+
Issue linkage
11+
- Addresses issue #21: ASN.1 improvements – https://github.com/whyoleg/cryptography-kotlin/issues/21
12+
- Support lists encoding: ✅ Implemented (SEQUENCE OF via `StructureKind.LIST`).
13+
- Support unknown keys: ✅ Implemented with `UnknownKeyAlgorithmIdentifier(algorithm, parameters)` and `Asn1Any` to preserve parameters.
14+
- Support default (optional values) encoding configuration: already ✅ upstream; unchanged here.
15+
- Support context-specific classes: already ✅ upstream; unchanged here. Added more negative tests for validation.
16+
17+
Details
18+
1) RFC 8410 OIDs and encoding rules
19+
- New helpers in `asn1/modules`:
20+
- `ObjectIdentifier.Companion.Ed25519` → 1.3.101.112
21+
- `ObjectIdentifier.Companion.Ed448` → 1.3.101.113
22+
- `ObjectIdentifier.Companion.X25519` → 1.3.101.110
23+
- `ObjectIdentifier.Companion.X448` → 1.3.101.111
24+
- `internal fun ObjectIdentifier.isRfc8410NoParams()` groups the four OIDs.
25+
- Encoder: `KeyAlgorithmIdentifierSerializer.encodeParameters` omits the "parameters" element for these OIDs (ABSENT). RSA remains explicit NULL; EC unchanged.
26+
- Decoder: tolerates both ABSENT and explicit NULL for these OIDs and constructs `UnknownKeyAlgorithmIdentifier`.
27+
28+
2) Unknown AlgorithmIdentifier preservation (round‑trip)
29+
- New `Asn1Any` captures raw TLV bytes of unknown parameters.
30+
- `UnknownKeyAlgorithmIdentifier` now stores `parameters: Any?` (previously `Nothing?`), using `Asn1Any` when present.
31+
- Encode behavior for unknown algorithms:
32+
- RFC 8410 OIDs → omit parameters (normalize to ABSENT).
33+
- Other OIDs → if `parameters` is `Asn1Any`, write it back as‑is; if null, omit.
34+
35+
3) SEQUENCE OF (lists)
36+
- `DerDecoder` and `DerEncoder` now handle `StructureKind.LIST` to encode/decode `SEQUENCE OF` values.
37+
38+
Tests
39+
- ASN.1 modules:
40+
- Encode: Ed25519/X25519 AlgorithmIdentifier encodes with ABSENT parameters; RSA encodes with explicit NULL.
41+
- Round‑trip normalization: decoding RFC 8410 NULL → re‑encodes to ABSENT.
42+
- Decode: Ed25519/X25519/Ed448/X448 accept both ABSENT and NULL.
43+
- SPKI decode: Ed25519 with ABSENT/NULL.
44+
- Core ASN.1:
45+
- Lists: `SEQUENCE OF INTEGER` encode/decode (+ empty list).
46+
- Negative tests: wrong tag for INTEGER; invalid long‑form length; context‑specific tag mismatch (IMPLICIT and EXPLICIT inner tag); BitString unused bits consistency.
47+
48+
CI
49+
- New workflow `.github/workflows/run-tests-asn1.yml` runs quick, focused matrix for ASN.1 core and modules: JVM, JS, Wasm Node, Linux x64.
50+
- Hooked into `run-checks.yml` (runs after build) to increase cross‑platform confidence without slowing the entire pipeline.
51+
52+
API / ABI notes
53+
- New public class: `dev.whyoleg.cryptography.serialization.asn1.Asn1Any` (core ASN.1 module).
54+
- `UnknownKeyAlgorithmIdentifier` signature changed:
55+
- Before: `UnknownKeyAlgorithmIdentifier(algorithm: ObjectIdentifier)` with `parameters: Nothing?`
56+
- After: `UnknownKeyAlgorithmIdentifier(algorithm: ObjectIdentifier, parameters: Any? = null)`
57+
- Source/ABI change in `cryptography-serialization-asn1-modules` (API files updated). Typical use sites constructing unknown identifiers remain source‑compatible if they do not reference the old `parameters` type; call sites with 1‑arg constructor continue to work.
58+
59+
Motivation and outcomes
60+
- RFC 8410 compliance: DER outputs match the spec (parameters ABSENT) while accepting explicit NULL in inputs seen in the wild.
61+
- Unknown keys are decodable and round‑trippable, making the codec resilient to extensions without schema updates.
62+
- SEQUENCE OF support unlocks more ASN.1 constructs and improves parity with real‑world structures.
63+
64+
Examples
65+
- Ed25519 AlgorithmIdentifier encodes to `30 05 06 03 2B 65 70` (no parameters element).
66+
- X25519 AlgorithmIdentifier encodes to `30 05 06 03 2B 65 6E`.
67+
- RSA AlgorithmIdentifier continues to encode with explicit NULL.
68+
69+
Backward compatibility notes
70+
- If external code relied on `UnknownKeyAlgorithmIdentifier.parameters` being always `null`, it may now hold `Asn1Any` for unknown algorithms with present parameters. Consumers can ignore `parameters` or recognize `Asn1Any` to access raw TLV bytes when needed.
71+
72+
Checklist against #21
73+
- [x] Support lists encoding (SEQUENCE OF)
74+
- [x] Support unknown keys (decode + preserve parameters)
75+
- [x] Support default (optional values) encoding configuration (already upstream)
76+
- [x] Support context-specific classes (already upstream; added negative tests)
77+

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+

0 commit comments

Comments
 (0)