Skip to content

Commit 77e7300

Browse files
committed
asn1(tests): add RFC 8410 encode/decode and round‑trip tests; SPKI decode; list encoding; negative validations
1 parent 5ca6e12 commit 77e7300

File tree

5 files changed

+294
-0
lines changed

5 files changed

+294
-0
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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.*
8+
import kotlinx.serialization.decodeFromByteArray
9+
import kotlin.test.*
10+
11+
class KeyAlgorithmIdentifierDecodeTest {
12+
13+
private fun String.hexToBytes(): ByteArray {
14+
check(length % 2 == 0) { "Invalid hex length" }
15+
return ByteArray(length / 2) { i ->
16+
val hi = this[i * 2].digitToInt(16)
17+
val lo = this[i * 2 + 1].digitToInt(16)
18+
((hi shl 4) or lo).toByte()
19+
}
20+
}
21+
22+
@Test
23+
fun decode_Ed25519_absentParameters() {
24+
// SEQUENCE { algorithm OBJECT IDENTIFIER 1.3.101.112 } (no parameters element)
25+
val bytes = "300506032B6570".hexToBytes()
26+
val id = Der.decodeFromByteArray<KeyAlgorithmIdentifier>(bytes)
27+
assertTrue(id is UnknownKeyAlgorithmIdentifier)
28+
assertEquals(ObjectIdentifier.Ed25519, id.algorithm)
29+
}
30+
31+
@Test
32+
fun decode_Ed25519_nullParameters() {
33+
// SEQUENCE { algorithm OBJECT IDENTIFIER 1.3.101.112, parameters NULL }
34+
val bytes = "300706032B65700500".hexToBytes()
35+
val id = Der.decodeFromByteArray<KeyAlgorithmIdentifier>(bytes)
36+
assertTrue(id is UnknownKeyAlgorithmIdentifier)
37+
assertEquals(ObjectIdentifier.Ed25519, id.algorithm)
38+
}
39+
40+
@Test
41+
fun decode_X25519_absentParameters() {
42+
// SEQUENCE { algorithm OBJECT IDENTIFIER 1.3.101.110 } (no parameters element)
43+
val bytes = "300506032B656E".hexToBytes()
44+
val id = Der.decodeFromByteArray<KeyAlgorithmIdentifier>(bytes)
45+
assertTrue(id is UnknownKeyAlgorithmIdentifier)
46+
assertEquals(ObjectIdentifier.X25519, id.algorithm)
47+
}
48+
49+
@Test
50+
fun decode_X25519_nullParameters() {
51+
// SEQUENCE { algorithm OBJECT IDENTIFIER 1.3.101.110, parameters NULL }
52+
val bytes = "300706032B656E0500".hexToBytes()
53+
val id = Der.decodeFromByteArray<KeyAlgorithmIdentifier>(bytes)
54+
assertTrue(id is UnknownKeyAlgorithmIdentifier)
55+
assertEquals(ObjectIdentifier.X25519, id.algorithm)
56+
}
57+
58+
@Test
59+
fun decode_Ed448_absentParameters() {
60+
val bytes = "300506032B6571".hexToBytes()
61+
val id = Der.decodeFromByteArray<KeyAlgorithmIdentifier>(bytes)
62+
assertTrue(id is UnknownKeyAlgorithmIdentifier)
63+
assertEquals(ObjectIdentifier.Ed448, id.algorithm)
64+
}
65+
66+
@Test
67+
fun decode_Ed448_nullParameters() {
68+
val bytes = "300706032B65710500".hexToBytes()
69+
val id = Der.decodeFromByteArray<KeyAlgorithmIdentifier>(bytes)
70+
assertTrue(id is UnknownKeyAlgorithmIdentifier)
71+
assertEquals(ObjectIdentifier.Ed448, id.algorithm)
72+
}
73+
74+
@Test
75+
fun decode_X448_absentParameters() {
76+
val bytes = "300506032B6570".replace("70","6F").hexToBytes() // 1.3.101.111
77+
val id = Der.decodeFromByteArray<KeyAlgorithmIdentifier>(bytes)
78+
assertTrue(id is UnknownKeyAlgorithmIdentifier)
79+
assertEquals(ObjectIdentifier.X448, id.algorithm)
80+
}
81+
82+
@Test
83+
fun decode_X448_nullParameters() {
84+
val bytes = "300706032B656F0500".hexToBytes()
85+
val id = Der.decodeFromByteArray<KeyAlgorithmIdentifier>(bytes)
86+
assertTrue(id is UnknownKeyAlgorithmIdentifier)
87+
assertEquals(ObjectIdentifier.X448, id.algorithm)
88+
}
89+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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.*
8+
import kotlinx.serialization.encodeToByteArray
9+
import kotlinx.serialization.decodeFromByteArray
10+
import kotlin.test.*
11+
12+
class KeyAlgorithmIdentifierEncodeTest {
13+
14+
private fun ByteArray.toHex() = joinToString("") { (it.toInt() and 0xFF).toString(16).padStart(2, '0') }
15+
private fun String.hexToBytes(): ByteArray {
16+
check(length % 2 == 0)
17+
return ByteArray(length / 2) { i ->
18+
val hi = this[i * 2].digitToInt(16)
19+
val lo = this[i * 2 + 1].digitToInt(16)
20+
((hi shl 4) or lo).toByte()
21+
}
22+
}
23+
24+
@Test
25+
fun encode_Ed25519_absentParameters() {
26+
val id: KeyAlgorithmIdentifier = UnknownKeyAlgorithmIdentifier(ObjectIdentifier.Ed25519)
27+
val bytes = Der.encodeToByteArray(id)
28+
assertEquals("300506032b6570", bytes.toHex())
29+
}
30+
31+
@Test
32+
fun encode_X25519_absentParameters() {
33+
val id: KeyAlgorithmIdentifier = UnknownKeyAlgorithmIdentifier(ObjectIdentifier.X25519)
34+
val bytes = Der.encodeToByteArray(id)
35+
assertEquals("300506032b656e", bytes.toHex())
36+
}
37+
38+
@Test
39+
fun encode_RSA_nullParameters() {
40+
val id: KeyAlgorithmIdentifier = RsaKeyAlgorithmIdentifier
41+
val bytes = Der.encodeToByteArray(id)
42+
assertEquals("300d06092a864886f70d0101010500", bytes.toHex())
43+
}
44+
45+
@Test
46+
fun roundTrip_Ed25519_null_normalizedToAbsent() {
47+
val withNull = "300706032b65700500".hexToBytes()
48+
val id = Der.decodeFromByteArray<KeyAlgorithmIdentifier>(withNull)
49+
val reencoded = Der.encodeToByteArray(id)
50+
assertEquals("300506032b6570", reencoded.toHex())
51+
}
52+
}
53+
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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.*
8+
import kotlinx.serialization.decodeFromByteArray
9+
import kotlin.test.*
10+
11+
class SubjectPublicKeyInfoRfc8410Test {
12+
private fun String.hexToBytes(): ByteArray {
13+
check(length % 2 == 0)
14+
return ByteArray(length / 2) { i ->
15+
val hi = this[i * 2].digitToInt(16)
16+
val lo = this[i * 2 + 1].digitToInt(16)
17+
((hi shl 4) or lo).toByte()
18+
}
19+
}
20+
21+
@Test
22+
fun spki_Ed25519_absentParameters() {
23+
val bytes = "300a300506032b6570030100".hexToBytes()
24+
val spki = Der.decodeFromByteArray<SubjectPublicKeyInfo>(bytes)
25+
assertEquals(ObjectIdentifier.Ed25519, spki.algorithm.algorithm)
26+
}
27+
28+
@Test
29+
fun spki_Ed25519_nullParameters() {
30+
val bytes = "300c300706032b65700500030100".hexToBytes()
31+
val spki = Der.decodeFromByteArray<SubjectPublicKeyInfo>(bytes)
32+
assertEquals(ObjectIdentifier.Ed25519, spki.algorithm.algorithm)
33+
}
34+
}
35+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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.builtins.ListSerializer
8+
import kotlinx.serialization.builtins.serializer
9+
import kotlin.test.*
10+
11+
class ListEncodingTest {
12+
private fun ByteArray.toHex() = joinToString("") { (it.toInt() and 0xFF).toString(16).padStart(2, '0') }
13+
14+
@Test
15+
fun encode_sequenceOf_int_topLevel() {
16+
val list = listOf(1, 2, 3)
17+
val bytes = Der.encodeToByteArray(ListSerializer(Int.serializer()), list)
18+
assertEquals("3009020101020102020103", bytes.toHex())
19+
}
20+
21+
@Test
22+
fun encode_sequenceOf_int_empty() {
23+
val list = emptyList<Int>()
24+
val bytes = Der.encodeToByteArray(ListSerializer(Int.serializer()), list)
25+
assertEquals("3000", bytes.toHex())
26+
}
27+
28+
@Test
29+
fun decode_sequenceOf_int_topLevel() {
30+
val bytes = "3009020101020102020103".chunked(2).map { it.toInt(16).toByte() }.toByteArray()
31+
val list = Der.decodeFromByteArray(ListSerializer(Int.serializer()), bytes)
32+
assertEquals(listOf(1, 2, 3), list)
33+
}
34+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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 dev.whyoleg.cryptography.serialization.asn1.ContextSpecificTag.*
8+
import kotlinx.serialization.Serializable
9+
import kotlinx.serialization.decodeFromByteArray
10+
import kotlin.test.*
11+
12+
class NegativeDecodingTest {
13+
14+
@Test
15+
fun wrongTagForInteger() {
16+
// OCTET STRING (0x04) with one byte content 0x01
17+
val bytes = byteArrayOf(0x04, 0x01, 0x01).map { it.toByte() }.toByteArray()
18+
assertFailsWith<IllegalStateException> {
19+
Der.decodeFromByteArray<Int>(bytes)
20+
}
21+
}
22+
23+
@Test
24+
fun invalidLengthZeroInLongForm() {
25+
// OID (0x06) with long-form length 0x82 0x00 0x00 -> illegal zero length
26+
val bytes = byteArrayOf(0x06, 0x82.toByte(), 0x00, 0x00).map { it.toByte() }.toByteArray()
27+
assertFailsWith<IllegalStateException> {
28+
Der.decodeFromByteArray<ObjectIdentifier>(bytes)
29+
}
30+
}
31+
32+
@Serializable
33+
class MandatoryImplicit(
34+
@ContextSpecificTag(0, TagType.IMPLICIT)
35+
val x: Int,
36+
)
37+
38+
@Test
39+
fun contextSpecificMandatoryTagMismatch() {
40+
// Encoded value only for tag [1] IMPLICIT with INTEGER 8
41+
val sequence = byteArrayOf(0x30, 0x03, 0x81.toByte(), 0x01, 0x08).map { it.toByte() }.toByteArray()
42+
assertFailsWith<IllegalStateException> {
43+
Der.decodeFromByteArray<MandatoryImplicit>(sequence)
44+
}
45+
}
46+
47+
@Serializable
48+
class ExplicitInt(
49+
@ContextSpecificTag(0, TagType.EXPLICIT)
50+
val x: Int,
51+
)
52+
53+
@Test
54+
fun contextSpecificExplicitInnerTagMismatch() {
55+
// SEQUENCE { [0] EXPLICIT { OCTET STRING 0x01 } } but Int expects INTEGER inside EXPLICIT
56+
val seq = byteArrayOf(
57+
0x30, 0x05, // SEQUENCE, len 5
58+
0xA0.toByte(), 0x03, // [0] EXPLICIT, len 3
59+
0x04, 0x01, 0x01 // OCTET STRING, len 1, 0x01
60+
)
61+
assertFailsWith<IllegalStateException> {
62+
Der.decodeFromByteArray<ExplicitInt>(seq)
63+
}
64+
}
65+
66+
@Test
67+
fun bitStringEmptyWithNonZeroUnusedBits() {
68+
// BIT STRING: length 1, unusedBits = 1, no payload
69+
val bs = byteArrayOf(0x03, 0x01, 0x01)
70+
assertFailsWith<IllegalStateException> {
71+
Der.decodeFromByteArray<BitArray>(bs)
72+
}
73+
}
74+
75+
@Test
76+
fun bitStringUnusedBitsExceedsTrailingZeros() {
77+
// BIT STRING: unusedBits=1, payload last byte 0x01 (no trailing zeros)
78+
val bs = byteArrayOf(0x03, 0x02, 0x01, 0x01)
79+
assertFailsWith<IllegalStateException> {
80+
Der.decodeFromByteArray<BitArray>(bs)
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)