Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consume only the first ASN.1 Element when parsing #128

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
30 changes: 21 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,31 +384,43 @@ Which results in the following output:

The magic shown above is based on a from-scratch 100% KMP implementation of an ASN.1 encoder and parser.
To parse any DER-encoded ASN.1 structure, call `Asn1Element.parse(derBytes)`, which will result in exactly a single
`Asn1Element`.
`Asn1Element`.
In addition, `Asn1Element.parseWithRemainder(derBytes)` returns both the parsed ASN.1 element from the passed bytes' start
and the remaining bytes.
It can be re-encoded (and yes, it is a true re-encoding, since the original bytes are discarded after decoding) by
accessing the lazily evaluated `.derEncoded` property.

**Note that decoding operations will throw exceptions if invalid data is provided!**

A parsed `Asn1Element` can either be a primitive (whose tag and value can be read) or a structure (like a set or
sequence) whose child
nodes can be processed as desired. Subclasses of `Asn1Element` reflect this:
sequence) whose child nodes can be processed as desired. Subclasses of `Asn1Element` reflect this:

* `Asn1Primitive`
* `Asn1BitString` (for convenience)
* `Asn1PrimitiveOctetString` (for convenience)
* `Asn1Structure`
* `Asn1Set`
* `Asn1Sequence`

* `Asn1Sequence` and `Asn1SequenceOf`
* `Asn1Set` and `Asn1SetOf` (sorting children by default)
* `Asn1EncapsulatingOctetString` (tagged as OCTET STRING, containing a valid ASN.1 structure or primitive)
* `Asn1ExplicitlyTagged` (user-specified tag + CONTEXT_SPECIFIC + CONSTRUCTED)
* `Asn1CustomStructure` (any other CONSTRUCTED tag not fitting the above options. CONSTRUCTED bit may be overridden)

Convenience wrappers exist, to cast to any subtype (e.g. `.asSequence()`). These shorthand functions throw an `Asn1Exception`
if a cast is not possible.
Any complex data structure (such as CSR, public key, certificate, …) implements `Asn1Encodable`, which means you can:

* encapsulate it into an ASN.1 Tree by calling `.encodeToTlv()`
* directly get a DER-encoded byte array through the `.encodetoDer()` function

To also suport going the other way, the companion objects of these complex classes implement `Asn1Decodable`, which
To also support going the other way, the companion objects of these complex classes implement `Asn1Decodable`, which
allows for

* directly parsing DER-encoded byte arrays by calling `.decodeFromDer(bytes)`
* processing an `Asn1Element` by calling `.fromTlv(src)`
* directly parsing DER-encoded byte arrays by calling `.decodeFromDer(bytes)` and `.decodeFromDerHexString`
* processing an `Asn1Element` by calling `.decodefromTlv(src)`

Both encoding and decoding functions come in two _safe_ (i.e. non-throwing) variants:
* `…Safe()` which returns a [KmmResult](https://github.com/a-sit-plus/kmmresult)
* `…orNull()` which returns null on error

#### Decoding Values

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 @@ -41,7 +41,8 @@ sealed class Asn1Element(

companion object {
/**
* Convenience method to directly parse a HEX-string representation of DER-encoded data
* Convenience method to directly parse a HEX-string representation of DER-encoded data.
* Ignores and strips all whitespace.
* @throws [Throwable] all sorts of errors on invalid input
*/
@Throws(Throwable::class)
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
Loading