Skip to content

Commit

Permalink
integrate ASN.1
Browse files Browse the repository at this point in the history
  • Loading branch information
gnarea committed Sep 12, 2023
1 parent ba98760 commit 055db87
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 17 deletions.
13 changes: 12 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ android {
abortOnError true
lintConfig file('lint.xml')
}
testOptions {
unitTests.all {
useJUnitPlatform()
}
}
}

dependencies {
Expand All @@ -72,6 +77,9 @@ dependencies {
// Awala
implementation 'com.github.relaycorp:awala-endpoint-android:1.13.22'

// Letro messaging
implementation("org.bouncycastle:bcprov-jdk15on:1.70")

// Compose
implementation platform('androidx.compose:compose-bom:2023.06.01')
implementation 'androidx.compose.ui:ui'
Expand All @@ -97,13 +105,16 @@ dependencies {
androidTestImplementation "androidx.room:room-testing:$room_version"

// Testing
testImplementation 'junit:junit:4.13.2'
def junitVersion = "5.8.2"
testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation platform('androidx.compose:compose-bom:2023.06.01')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
testImplementation 'io.kotest:kotest-assertions-core:5.7.2'
}

// Allow references to generated code
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package tech.relaycorp.letro.utils.asn1

class ASN1Exception(message: String, cause: Throwable? = null) : Exception(message, cause)
73 changes: 73 additions & 0 deletions app/src/main/java/tech/relaycorp/letro/utils/asn1/ASN1Utils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package tech.relaycorp.letro.utils.asn1

import java.io.IOException
import org.bouncycastle.asn1.ASN1Encodable
import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1InputStream
import org.bouncycastle.asn1.ASN1OctetString
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.ASN1TaggedObject
import org.bouncycastle.asn1.ASN1VisibleString
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.DERTaggedObject

internal object ASN1Utils {
fun makeSequence(items: List<ASN1Encodable>, explicitTagging: Boolean = true): DERSequence {
val messagesVector = ASN1EncodableVector(items.size)
val finalItems = if (explicitTagging) items else items.mapIndexed { index, item ->
DERTaggedObject(false, index, item)
}
finalItems.forEach { messagesVector.add(it) }
return DERSequence(messagesVector)
}

fun serializeSequence(items: List<ASN1Encodable>, explicitTagging: Boolean = true): ByteArray {
return makeSequence(items, explicitTagging).encoded
}

@Throws(ASN1Exception::class)
fun deserializeSequence(serialization: ByteArray): ASN1Sequence {
if (serialization.isEmpty()) {
throw ASN1Exception("Value is empty")
}
val asn1InputStream = ASN1InputStream(serialization)
val asn1Value = try {
asn1InputStream.readObject()
} catch (_: IOException) {
throw ASN1Exception("Value is not DER-encoded")
}
return try {
ASN1Sequence.getInstance(asn1Value)
} catch (_: IllegalArgumentException) {
throw ASN1Exception("Value is not an ASN.1 sequence")
}
}

@Throws(ASN1Exception::class)
inline fun <reified T : ASN1Encodable> deserializeHomogeneousSequence(
serialization: ByteArray
): Array<T> {
val sequence = deserializeSequence(serialization)
return sequence.map {
if (it !is T) {
throw ASN1Exception(
"Sequence contains an item of an unexpected type " +
"(${it::class.java.simpleName})"
)
}
@Suppress("USELESS_CAST")
it as T
}.toTypedArray()
}

@Throws(ASN1Exception::class)
fun deserializeHeterogeneousSequence(serialization: ByteArray): Array<ASN1TaggedObject> =
deserializeHomogeneousSequence(serialization)

fun getVisibleString(visibleString: ASN1TaggedObject): ASN1VisibleString =
ASN1VisibleString.getInstance(visibleString, false)

fun getOctetString(octetString: ASN1TaggedObject): ASN1OctetString =
DEROctetString.getInstance(octetString, false)
}
16 changes: 0 additions & 16 deletions app/src/test/java/tech/relaycorp/letro/ExampleUnitTest.kt

This file was deleted.

158 changes: 158 additions & 0 deletions app/src/test/java/tech/relaycorp/letro/utils/asn1/ASN1UtilsTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package tech.relaycorp.letro.utils.asn1

import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.beInstanceOf
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.ASN1StreamParser
import org.bouncycastle.asn1.ASN1TaggedObject
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.DEROctetStringParser
import org.bouncycastle.asn1.DERVisibleString
import org.bouncycastle.asn1.DLSequenceParser
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows

internal class ASN1UtilsTest {
val visibleString = DERVisibleString("foo")
val octetString = DEROctetString("bar".toByteArray())

@Nested
inner class MakeSequence {
@Test
fun `Values should be explicitly tagged by default`() {
val sequence = ASN1Utils.makeSequence(listOf(visibleString, octetString))

sequence.size() shouldBe 2

val item1 = sequence.getObjectAt(0)
item1 should beInstanceOf<DERVisibleString>()
visibleString.string shouldBe (item1 as DERVisibleString).string

val item2 = sequence.getObjectAt(1)
item2 should beInstanceOf<DEROctetString>()
octetString.octets shouldBe (item2 as DEROctetString).octets
}

@Test
fun `Implicitly-tagged values should be supported`() {
val sequence = ASN1Utils.makeSequence(listOf(visibleString, octetString), false)

sequence.size() shouldBe 2

val item1 = ASN1Utils.getVisibleString(sequence.getObjectAt(0) as ASN1TaggedObject)
visibleString.string shouldBe item1.string

val item2 = ASN1Utils.getOctetString(sequence.getObjectAt(1) as ASN1TaggedObject)
octetString.octets shouldBe item2.octets
}
}

@Nested
inner class SerializeSequence {
@Test
fun `Values should be explicitly tagged by default`() {
val serialization = ASN1Utils.serializeSequence(listOf(visibleString, octetString))

val parser = ASN1StreamParser(serialization)
val sequence = parser.readObject() as DLSequenceParser

val item1 = sequence.readObject()
item1 should beInstanceOf<DERVisibleString>()
visibleString.string shouldBe (item1 as DERVisibleString).string

val item2 = sequence.readObject()
item2 should beInstanceOf<DEROctetStringParser>()
octetString.octets shouldBe
((item2 as DEROctetStringParser).loadedObject as DEROctetString).octets
}

@Test
fun `Implicitly-tagged values should be supported`() {
val serialization =
ASN1Utils.serializeSequence(listOf(visibleString, octetString), false)

val parser = ASN1StreamParser(serialization)
val sequence =
ASN1Sequence.getInstance(parser.readObject() as DLSequenceParser).toArray()

val item1 = ASN1Utils.getVisibleString(sequence[0] as ASN1TaggedObject)
visibleString.string shouldBe item1.string

val item2 = ASN1Utils.getOctetString(sequence[1] as ASN1TaggedObject)
octetString.octets shouldBe item2.octets
}
}

@Nested
inner class DeserializeSequence {
@Test
fun `Value should be refused if it's empty`() {
val exception = assertThrows<ASN1Exception> {
ASN1Utils.deserializeHeterogeneousSequence(byteArrayOf())
}

"Value is empty" shouldBe exception.message
}

@Test
fun `Value should be refused if it's not DER-encoded`() {
val exception = assertThrows<ASN1Exception> {
ASN1Utils.deserializeHeterogeneousSequence("a".toByteArray())
}

"Value is not DER-encoded" shouldBe exception.message
}

@Test
fun `Value should be refused if it's not a sequence`() {
val serialization = DERVisibleString("hey").encoded

val exception = assertThrows<ASN1Exception> {
ASN1Utils.deserializeHeterogeneousSequence(serialization)
}

"Value is not an ASN.1 sequence" shouldBe exception.message
}

@Test
fun `Explicitly tagged items should be deserialized with their corresponding types`() {
val serialization = ASN1Utils.serializeSequence(listOf(visibleString, visibleString))

val sequence = ASN1Utils.deserializeHomogeneousSequence<DERVisibleString>(serialization)

2 shouldBe sequence.size
val value1Deserialized = sequence.first()
visibleString shouldBe value1Deserialized
val value2Deserialized = sequence.last()
visibleString shouldBe value2Deserialized
}

@Test
fun `Explicitly tagged items with unexpected types should be refused`() {
val serialization = ASN1Utils.serializeSequence(listOf(visibleString, octetString))

val exception = assertThrows<ASN1Exception> {
ASN1Utils.deserializeHomogeneousSequence<DERVisibleString>(serialization)
}

exception.message shouldBe
"Sequence contains an item of an unexpected type " +
"(${octetString::class.java.simpleName})"
}

@Test
fun `Implicitly tagged items should be deserialized with their corresponding types`() {
val serialization =
ASN1Utils.serializeSequence(listOf(visibleString, octetString), false)

val sequence = ASN1Utils.deserializeHeterogeneousSequence(serialization)

2 shouldBe sequence.size
visibleString.octets shouldBe
ASN1Utils.getVisibleString(sequence.first()).octets
octetString.octets shouldBe ASN1Utils.getOctetString(sequence[1]).octets
}
}
}

0 comments on commit 055db87

Please sign in to comment.