Skip to content

Commit

Permalink
Consume only the first ASN.1 Element when parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
JesusMcCloud committed Sep 16, 2024
1 parent b8cc4a2 commit 05a0a44
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 4 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
* `Iterable<Byte>.decodeAsn1VarUInt()`
* `ByteArray.decodeAsn1VarUInt()`
* Revamp implicit tagging
* Consume only the first `Asn1Element.parse()` only consumes the first parsable element and
`Asn1Element.parserWithRemainder()` additionally returns the remaining bytes for convenience

## 3.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ import kotlinx.datetime.Instant
import kotlin.experimental.and
import kotlin.math.ceil

/**
* Result of parsing a single, toplevel [Asn1Element] from a bytearray
*/
typealias Asn1Parsed = Pair<Asn1Element, ByteArray>

/**
* The parsed [Asn1Element]
*/
val Asn1Parsed.element get() = first

/**
* The remainder of the underlying bytearray (empty if, everything was consumed)
*/
val Asn1Parsed.remainingBytes get() = second

/**
* Parses the provides [input] into a single [Asn1Element]
Expand All @@ -23,17 +37,30 @@ import kotlin.math.ceil
* @throws Asn1Exception on invalid input or if more than a single root structure was contained in the [input]
*/
@Throws(Asn1Exception::class)
fun Asn1Element.Companion.parse(input: ByteArray) = Asn1Reader(input).doParse().let {
fun Asn1Element.Companion.parse(input: ByteArray): Asn1Element = Asn1Reader(input).doParse(single = true).let {
if (it.size != 1) throw Asn1StructuralException("Multiple ASN.1 structures found")
it.first()
}

private class Asn1Reader(input: ByteArray) {
/**
* Parses the provides [input] into a single [Asn1Element]
* @return the [Asn1Parsed] containing an element and remaining bytes
*
* @throws Asn1Exception on invalid input or if more than a single root structure was contained in the [input]
*/
//this only makes sense until we switch to kotlinx.io
@Throws(Asn1Exception::class)
fun Asn1Element.Companion.parseWithRemainder(input: ByteArray): Asn1Parsed = parse(input).let {
it to input.drop(it.overallLength).toByteArray()
}


private class Asn1Reader(private val input: ByteArray) {

private var rest = input

@Throws(Asn1Exception::class)
fun doParse(): List<Asn1Element> = runRethrowing {
fun doParse(single: Boolean = false): List<Asn1Element> = runRethrowing {
val result = mutableListOf<Asn1Element>()
while (rest.isNotEmpty()) {
val tlv = read()
Expand All @@ -54,7 +81,7 @@ private class Asn1Reader(input: ByteArray) {
} else if (tlv.tag.isConstructed) { //custom tags, we don't know if it is a SET OF, SET, SEQUENCE,… so we default to sequence semantics
result.add(Asn1CustomStructure(Asn1Reader(tlv.content).doParse(), tlv.tag.tagValue, tlv.tagClass))
} else result.add(Asn1Primitive(tlv.tag, tlv.content))

if (single) return result
}
return result
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import java.io.FileReader
import java.io.InputStream
import java.security.cert.CertificateFactory
import java.util.*
import kotlin.random.Random
import kotlin.random.nextInt
import java.security.cert.X509Certificate as JcaCertificate


Expand All @@ -34,6 +36,12 @@ class X509CertParserTest : FreeSpec({
val derBytes =
javaClass.classLoader.getResourceAsStream("certs/ok-uniqueid-incomplete-byte.der").readBytes()
X509Certificate.decodeFromDer(derBytes)

val garbage = Random.nextBytes(Random.nextInt(0..128))
Asn1Element.parseWithRemainder(derBytes + garbage).let { (parsed, remainder) ->
parsed.derEncoded shouldBe derBytes
remainder shouldBe garbage
}
}


Expand All @@ -52,6 +60,12 @@ class X509CertParserTest : FreeSpec({
cert.encodeToTlv().derEncoded shouldBe jcaCert.encoded

cert shouldBe X509Certificate.decodeFromByteArray(certBytes)

val garbage = Random.nextBytes(Random.nextInt(0..128))
Asn1Element.parseWithRemainder(certBytes + garbage).let { (parsed, remainder) ->
parsed.derEncoded shouldBe certBytes
remainder shouldBe garbage
}
}
}
}
Expand Down Expand Up @@ -106,6 +120,12 @@ class X509CertParserTest : FreeSpec({
) {
own shouldBe crt.encoded
parsed shouldBe X509Certificate.decodeFromByteArray(crt.encoded)

val garbage = Random.nextBytes(Random.nextInt(0..128))
Asn1Element.parseWithRemainder(crt.encoded + garbage).let { (parsed, remainder) ->
parsed.derEncoded shouldBe own
remainder shouldBe garbage
}
}
}
}
Expand All @@ -122,6 +142,12 @@ class X509CertParserTest : FreeSpec({
val src = Asn1Element.parse(it.second) as Asn1Sequence
val decoded = X509Certificate.decodeFromTlv(src)
decoded shouldBe X509Certificate.decodeFromByteArray(it.second)

val garbage = Random.nextBytes(Random.nextInt(0..128))
Asn1Element.parseWithRemainder(it.second + garbage).let { (parsed, remainder) ->
parsed.derEncoded shouldBe it.second
remainder shouldBe garbage
}
}
}
"Faulty certs should glitch out" - {
Expand Down Expand Up @@ -159,6 +185,12 @@ class X509CertParserTest : FreeSpec({

jcaCert.encoded shouldBe encodedSrc
cert.encodeToTlv().derEncoded shouldBe encodedSrc

val garbage = Random.nextBytes(Random.nextInt(0..128))
Asn1Element.parseWithRemainder(jcaCert.encoded + garbage).let { (parsed, remainder) ->
parsed.derEncoded shouldBe jcaCert.encoded
remainder shouldBe garbage
}
}
}

Expand Down

0 comments on commit 05a0a44

Please sign in to comment.