Skip to content

Commit 5e38961

Browse files
committed
Add SUB_MESSAGES coding to distinguish IDB container messages from plain BYTES
Container messages (VISA, ETD, PROOF_OF_TESTING, etc.) now use explicit SUB_MESSAGES coding instead of overloading BYTES. Parsing validates the first byte against expected child tags before attempting DER-TLV parsing. IdbSeal now passes sub-messages through in getMessageByName/Tag/messageList.
1 parent bd17e1d commit 5e38961

7 files changed

Lines changed: 58 additions & 19 deletions

File tree

src/commonMain/kotlin/de/tsenger/vdstools/DataEncoder.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@ object DataEncoder {
486486
return when (coding) {
487487
MessageCoding.C40, MessageCoding.MRZ -> encodeC40(value as String)
488488
MessageCoding.UTF8_STRING -> (value as String).encodeToByteArray()
489-
MessageCoding.BYTES -> value as ByteArray
489+
MessageCoding.BYTES, MessageCoding.SUB_MESSAGES -> value as ByteArray
490490
MessageCoding.BYTE -> when (value) {
491491
is Int -> byteArrayOf((value and 0xFF).toByte())
492492
is Byte -> byteArrayOf(value)

src/commonMain/kotlin/de/tsenger/vdstools/generic/MessageCoding.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ enum class MessageCoding {
88
C40,
99
UTF8_STRING,
1010
BYTES,
11+
SUB_MESSAGES,
1112
BYTE,
1213
MASKED_DATE,
1314
DATE,

src/commonMain/kotlin/de/tsenger/vdstools/generic/MessageValue.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ sealed class MessageValue {
203203
}
204204

205205
MessageCoding.VALIDITY_DATES -> ValidityDatesValue.parse(bytes)
206-
MessageCoding.BYTES, MessageCoding.UNKNOWN -> BytesValue(bytes)
206+
MessageCoding.BYTES, MessageCoding.SUB_MESSAGES, MessageCoding.UNKNOWN -> BytesValue(bytes)
207207
}
208208
} catch (e: Exception) {
209209
// If decoding fails, fall back to raw bytes

src/commonMain/kotlin/de/tsenger/vdstools/idb/IdbMessageGroup.kt

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,17 @@ class IdbMessageGroup {
147147

148148
val childDefs = msgDef?.messages
149149
?: DataEncoder.idbMessageTypes.getMessageTypeDto(tagInt)?.messages
150-
val subMessages = if (!childDefs.isNullOrEmpty() && coding == MessageCoding.BYTES) {
151-
try {
152-
val childTlvs = DataEncoder.parseDerTLvs(derTlv.value)
153-
childTlvs.map { childTlv -> parseMessage(childTlv, childDefs) }
154-
} catch (_: Exception) {
150+
val subMessages = if (!childDefs.isNullOrEmpty() && coding == MessageCoding.SUB_MESSAGES) {
151+
val expectedTags = childDefs.map { it.tag.toByte() }.toSet()
152+
val firstByte = derTlv.value.firstOrNull()
153+
if (firstByte != null && firstByte in expectedTags) {
154+
try {
155+
val childTlvs = DataEncoder.parseDerTLvs(derTlv.value)
156+
childTlvs.map { childTlv -> parseMessage(childTlv, childDefs) }
157+
} catch (_: Exception) {
158+
emptyList()
159+
}
160+
} else {
155161
emptyList()
156162
}
157163
} else {

src/commonMain/kotlin/de/tsenger/vdstools/idb/IdbSeal.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,17 @@ class IdbSeal : Seal {
6565

6666
override fun getMessageByName(name: String): Message? {
6767
val idbMessage = payLoad.idbMessageGroup.getMessageByName(name) ?: return null
68-
return Message(idbMessage.tag, idbMessage.name, idbMessage.coding, idbMessage.value)
68+
return Message(idbMessage.tag, idbMessage.name, idbMessage.coding, idbMessage.value, idbMessage.messages)
6969
}
7070

7171
override fun getMessageByTag(tag: Int): Message? {
7272
val idbMessage = payLoad.idbMessageGroup.getMessageByTag(tag) ?: return null
73-
return Message(idbMessage.tag, idbMessage.name, idbMessage.coding, idbMessage.value)
73+
return Message(idbMessage.tag, idbMessage.name, idbMessage.coding, idbMessage.value, idbMessage.messages)
7474
}
7575

7676
override fun getMessageByTag(tag: String): Message? {
7777
val idbMessage = payLoad.idbMessageGroup.getMessageByTag(tag) ?: return null
78-
return Message(idbMessage.tag, idbMessage.name, idbMessage.coding, idbMessage.value)
78+
return Message(idbMessage.tag, idbMessage.name, idbMessage.coding, idbMessage.value, idbMessage.messages)
7979
}
8080

8181
/**
@@ -94,7 +94,7 @@ class IdbSeal : Seal {
9494

9595
override val messageList: List<Message>
9696
get() = payLoad.idbMessageGroup.messageList.map { idbMessage ->
97-
Message(idbMessage.tag, idbMessage.name, idbMessage.coding, idbMessage.value)
97+
Message(idbMessage.tag, idbMessage.name, idbMessage.coding, idbMessage.value, idbMessage.messages)
9898
}
9999

100100
override val signatureInfo: SignatureInfo?

src/commonMain/resources/IdbMessageTypes.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
{
33
"name": "VISA",
44
"tag": 1,
5-
"coding": "BYTES",
5+
"coding": "SUB_MESSAGES",
66
"minBytes": 62,
77
"maxBytes": 328,
88
"messages": [
@@ -67,7 +67,7 @@
6767
{
6868
"name": "EMERGENCY_TRAVEL_DOCUMENT",
6969
"tag": 2,
70-
"coding": "BYTES",
70+
"coding": "SUB_MESSAGES",
7171
"minBytes": 50,
7272
"maxBytes": 50,
7373
"messages": [
@@ -84,7 +84,7 @@
8484
{
8585
"name": "PROOF_OF_TESTING",
8686
"tag": 3,
87-
"coding": "BYTES",
87+
"coding": "SUB_MESSAGES",
8888
"minBytes": 68,
8989
"maxBytes": 659,
9090
"messages": [
@@ -221,7 +221,7 @@
221221
{
222222
"name": "PROOF_OF_VACCINATION",
223223
"tag": 4,
224-
"coding": "BYTES",
224+
"coding": "SUB_MESSAGES",
225225
"minBytes": 57,
226226
"maxBytes": 951,
227227
"messages": [
@@ -292,7 +292,7 @@
292292
{
293293
"name": "VACCINATION_EVENT",
294294
"tag": 9,
295-
"coding": "BYTES",
295+
"coding": "SUB_MESSAGES",
296296
"required": true,
297297
"minBytes": 44,
298298
"maxBytes": 842,
@@ -340,7 +340,7 @@
340340
{
341341
"name": "VACCINATION_DETAILS",
342342
"tag": 36,
343-
"coding": "BYTES",
343+
"coding": "SUB_MESSAGES",
344344
"required": true,
345345
"minBytes": 22,
346346
"maxBytes": 56,
@@ -418,7 +418,7 @@
418418
{
419419
"name": "PROOF_OF_RECOVERY",
420420
"tag": 5,
421-
"coding": "BYTES",
421+
"coding": "SUB_MESSAGES",
422422
"minBytes": 46,
423423
"maxBytes": 108,
424424
"messages": [
@@ -507,7 +507,7 @@
507507
{
508508
"name": "DIGITAL_TRAVEL_AUTHORIZATION",
509509
"tag": 6,
510-
"coding": "BYTES",
510+
"coding": "SUB_MESSAGES",
511511
"minBytes": 58,
512512
"maxBytes": 197,
513513
"messages": [

src/commonTest/kotlin/de/tsenger/vdstools/idb/IdbMessageGroupCommonTest.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import de.tsenger.vdstools.DataEncoder
44
import de.tsenger.vdstools.asn1.DerTlv
55
import de.tsenger.vdstools.idb.IdbMessageGroup.Companion.fromByteArray
66
import kotlinx.io.IOException
7+
import de.tsenger.vdstools.generic.MessageCoding
78
import kotlin.test.Test
89
import kotlin.test.assertEquals
910
import kotlin.test.assertNotNull
@@ -214,6 +215,37 @@ class IdbMessageGroupCommonTest {
214215
assertEquals("DE", country.value.toString())
215216
}
216217

218+
@Test
219+
fun testPlainBytesNotParsedAsSubMessages() {
220+
// EF_CARD_ACCESS (tag 10) has coding BYTES — even if the content happens to look like DER-TLV,
221+
// it must NOT be parsed as sub-messages
222+
val fakeContent = byteArrayOf(0x01, 0x04, 0xAA.toByte(), 0xBB.toByte(), 0xCC.toByte(), 0xDD.toByte())
223+
val derTlv = DerTlv(0x0A, fakeContent)
224+
val messageGroup = IdbMessageGroup(listOf(derTlv))
225+
226+
val msg = messageGroup.getMessageByTag(0x0A)
227+
assertNotNull(msg)
228+
assertEquals("EF_CARD_ACCESS", msg.name)
229+
assertEquals(MessageCoding.BYTES, msg.coding)
230+
assertTrue(msg.messages.isEmpty(), "BYTES-coded message must not have sub-messages")
231+
}
232+
233+
@Test
234+
fun testCorruptedSubMessageDataGracefullyFails() {
235+
// Build a container message (EMERGENCY_TRAVEL_DOCUMENT, tag 2) with corrupted data
236+
// that cannot be parsed as valid DER-TLV sub-messages
237+
val corruptedData = byteArrayOf(0x02, 0xFF.toByte(), 0x00, 0x01)
238+
val derTlv = DerTlv(0x02, corruptedData)
239+
val messageGroup = IdbMessageGroup(listOf(derTlv))
240+
241+
val msg = messageGroup.getMessageByTag(0x02)
242+
assertNotNull(msg)
243+
assertEquals("EMERGENCY_TRAVEL_DOCUMENT", msg.name)
244+
assertEquals(MessageCoding.SUB_MESSAGES, msg.coding)
245+
// Corrupted data should result in empty sub-messages, not an exception
246+
assertTrue(msg.messages.isEmpty(), "Corrupted sub-message data should result in empty messages list")
247+
}
248+
217249
@Test
218250
fun testVdsMessagesHaveEmptySubMessages() {
219251
// Non-container messages should have empty messages list

0 commit comments

Comments
 (0)