Skip to content

Commit

Permalink
Implement account creation message deserialisation
Browse files Browse the repository at this point in the history
  • Loading branch information
gnarea committed Sep 18, 2023
1 parent 39b8927 commit 3725bc5
Show file tree
Hide file tree
Showing 7 changed files with 465 additions and 94 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package tech.relaycorp.letro.server.messages

import org.bouncycastle.asn1.ASN1Encodable
import org.bouncycastle.asn1.ASN1TaggedObject
import org.bouncycastle.asn1.ASN1UTF8String
import tech.relaycorp.letro.utils.asn1.ASN1Exception
import tech.relaycorp.letro.utils.asn1.ASN1Utils
import tech.relaycorp.letro.utils.i18n.parseLocale
import java.util.Locale

/**
* This message signifies that a VeraId identifier has been created.
*
* See: https://docs.relaycorp.tech/letro-server/account-creation#account-creation-1
*/
class AccountCreation(
val requestedUserName: String,
val locale: Locale,
val assignedUserId: String,
val veraidBundle: ByteArray,
) {
companion object {
fun deserialise(serialised: ByteArray): AccountCreation {
val accountCreationSequence = try {
ASN1Utils.deserializeSequence(serialised)
} catch (exc: ASN1Exception) {
throw MalformedAccountCreationException(
"AccountCreation should be a DER-encoded sequence",
exc,
)
}
if (accountCreationSequence.size() < 4) {
throw MalformedAccountCreationException(
"AccountCreation SEQUENCE should have at least 4 items",
)
}

val requestedUserName = decodeRequestedUserName(accountCreationSequence.getObjectAt(0))
val locale = decodeLocale(accountCreationSequence.getObjectAt(1))
val assignedUserId = decodeAssignedUserId(accountCreationSequence.getObjectAt(2))
val veraidBundle = decodeVeraidBundle(accountCreationSequence.getObjectAt(3))
return AccountCreation(requestedUserName, locale, assignedUserId, veraidBundle)
}

private fun decodeRequestedUserName(userNameTagged: ASN1Encodable): String {
val requestedUserNameEncoded = try {
ASN1UTF8String.getInstance(userNameTagged as ASN1TaggedObject, false)
} catch (exc: RuntimeException) {
throw MalformedAccountCreationException(
"AccountCreation requestedUserName should be a DER-encoded UTF8String",
exc,
)
}
return requestedUserNameEncoded.string
}

private fun decodeLocale(localeTagged: ASN1Encodable): Locale {
val localeEncoded = try {
ASN1Utils.getVisibleString(localeTagged as ASN1TaggedObject)
} catch (exc: RuntimeException) {
throw MalformedAccountCreationException(
"AccountCreation locale should be a DER-encoded VisibleString",
exc,
)
}
return localeEncoded.string.parseLocale()
}

private fun decodeAssignedUserId(assignedUserIdTagged: ASN1Encodable): String {
val assignedUserIdEncoded = try {
ASN1UTF8String.getInstance(assignedUserIdTagged as ASN1TaggedObject, false)
} catch (exc: RuntimeException) {
throw MalformedAccountCreationException(
"AccountCreation assignedUserId should be a DER-encoded UTF8String",
exc,
)
}
return assignedUserIdEncoded.string
}

private fun decodeVeraidBundle(veraidBundleTagged: ASN1Encodable): ByteArray {
val veraidBundleEncoded = try {
ASN1Utils.getOctetString(veraidBundleTagged as ASN1TaggedObject)
} catch (exc: RuntimeException) {
throw MalformedAccountCreationException(
"AccountCreation veraidBundle should be a DER-encoded OCTET STRING",
exc,
)
}
return veraidBundleEncoded.octets
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import java.security.PrivateKey
import java.security.PublicKey
import java.util.Locale

/**
* This message signifies a Letro user's intention to create a VeraId identifier.
*
* See https://docs.relaycorp.tech/letro-server/account-creation#account-creation-request
*/
class AccountRequest(
val userName: String,
val locale: Locale,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package tech.relaycorp.letro.server.messages

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

import java.util.Locale

fun Locale.normaliseString(): String {
val languageCode = language.lowercase()
val countryCode = country.lowercase()
return if (languageCode.isEmpty()) {
""
} else if (countryCode.isEmpty()) {
languageCode
} else {
"$languageCode-$countryCode"
}
}

fun String.parseLocale(): Locale {
val localeParts = split("-")
val languageCode = localeParts[0].lowercase()
val countryCode = localeParts.getOrNull(1)?.uppercase() ?: ""
val variantCode = localeParts.getOrNull(2) ?: ""
return Locale(languageCode, countryCode, variantCode)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package tech.relaycorp.letro.server.messages

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.beInstanceOf
import org.bouncycastle.asn1.DERNull
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.DERTaggedObject
import org.bouncycastle.asn1.DERUTF8String
import org.bouncycastle.asn1.DERVisibleString
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import tech.relaycorp.letro.utils.asn1.ASN1Exception
import tech.relaycorp.letro.utils.asn1.ASN1Utils
import tech.relaycorp.letro.utils.i18n.normaliseString
import java.util.Locale

class AccountCreationTest {
val requestedUserName = "alice"
val locale = Locale("EN", "GB")
val assignedUserId = "[email protected]"
val veraidBundle = byteArrayOf(0x00)

val nonAsciiUsername = "久美子"
val nonAsciiDomainName = "はじめよう.みんな"

@Nested
inner class Deserialise {
@Test
fun `Serialisation should be a DER-encoded sequence`() {
val exception = shouldThrow<MalformedAccountCreationException> {
AccountCreation.deserialise(byteArrayOf(0x00))
}

exception.message shouldBe "AccountCreation should be a DER-encoded sequence"
exception.cause should beInstanceOf<ASN1Exception>()
}

@Test
fun `Sequence should have at least 4 items`() {
val malformedSerialisation = ASN1Utils.serializeSequence(
listOf(DERNull.INSTANCE, DERNull.INSTANCE, DERNull.INSTANCE),
false,
)

val exception = shouldThrow<MalformedAccountCreationException> {
AccountCreation.deserialise(malformedSerialisation)
}

exception.message shouldBe "AccountCreation SEQUENCE should have at least 4 items"
}

@Nested
inner class RequestedUserName {
@Test
fun `Should be a DER-encoded UTF8String`() {
val malformedSerialisation = DERSequence(
arrayOf(
DERTaggedObject(true, 0, DERNull.INSTANCE),
DERNull.INSTANCE,
DERNull.INSTANCE,
DERNull.INSTANCE,
),
).encoded

val exception = shouldThrow<MalformedAccountCreationException> {
AccountCreation.deserialise(malformedSerialisation)
}

exception.message shouldBe
"AccountCreation requestedUserName should be a DER-encoded UTF8String"
}

@Test
fun `Should be decoded as a UTF-8 string`() {
val serialisation = AccountCreation(
nonAsciiUsername,
locale,
assignedUserId,
veraidBundle,
).serialise()

val accountCreation = AccountCreation.deserialise(serialisation)

accountCreation.requestedUserName shouldBe nonAsciiUsername
}
}

@Nested
inner class Locale {
@Test
fun `Should be a DER-encoded VisibleString`() {
val malformedSerialisation = DERSequence(
arrayOf(
DERTaggedObject(false, 0, DERUTF8String(requestedUserName)),
DERTaggedObject(true, 0, DERVisibleString(locale.normaliseString())),
DERUTF8String(assignedUserId),
DEROctetString(veraidBundle),
),
).encoded

val exception = shouldThrow<MalformedAccountCreationException> {
AccountCreation.deserialise(malformedSerialisation)
}

exception.message shouldBe
"AccountCreation locale should be a DER-encoded VisibleString"
}

@Test
fun `Should be decoded as a Locale`() {
val serialisation = AccountCreation(
requestedUserName,
locale,
assignedUserId,
veraidBundle,
).serialise()

val accountCreation = AccountCreation.deserialise(serialisation)

accountCreation.locale shouldBe locale
}
}

@Nested
inner class AssignedUserId {
@Test
fun `Should be a DER-encoded UTF8String`() {
val malformedSerialisation = DERSequence(
arrayOf(
DERTaggedObject(false, 0, DERUTF8String(requestedUserName)),
DERTaggedObject(false, 1, DERVisibleString(locale.normaliseString())),
DERTaggedObject(true, 2, DERVisibleString(assignedUserId)),
DERNull.INSTANCE,
),
).encoded

val exception = shouldThrow<MalformedAccountCreationException> {
AccountCreation.deserialise(malformedSerialisation)
}

exception.message shouldBe
"AccountCreation assignedUserId should be a DER-encoded UTF8String"
}

@Test
fun `Should be decoded as a UTF-8 string`() {
val nonAsciiAssignedUserId = "$nonAsciiUsername@$nonAsciiDomainName"
val serialisation = AccountCreation(
requestedUserName,
locale,
nonAsciiAssignedUserId,
veraidBundle,
).serialise()

val accountCreation = AccountCreation.deserialise(serialisation)

accountCreation.assignedUserId shouldBe nonAsciiAssignedUserId
}
}

@Nested
inner class VeraidBundle {
@Test
fun `Should be a DER-encoded OCTET STRING`() {
val malformedSerialisation = DERSequence(
arrayOf(
DERTaggedObject(false, 0, DERUTF8String(requestedUserName)),
DERTaggedObject(false, 1, DERVisibleString(locale.normaliseString())),
DERTaggedObject(false, 2, DERVisibleString(assignedUserId)),
DERTaggedObject(true, 3, DERNull.INSTANCE),
),
).encoded

val exception = shouldThrow<MalformedAccountCreationException> {
AccountCreation.deserialise(malformedSerialisation)
}

exception.message shouldBe
"AccountCreation veraidBundle should be a DER-encoded OCTET STRING"
}

@Test
fun `Should be decoded as a DER-encoded OCTET STRING`() {
val serialisation = AccountCreation(
requestedUserName,
locale,
assignedUserId,
veraidBundle,
).serialise()

val accountCreation = AccountCreation.deserialise(serialisation)

accountCreation.veraidBundle shouldBe veraidBundle
}
}
}

private fun AccountCreation.serialise(): ByteArray {
return ASN1Utils.serializeSequence(
listOf(
DERUTF8String(requestedUserName),
DERVisibleString(locale.normaliseString()),
DERUTF8String(assignedUserId),
DEROctetString(veraidBundle),
),
false,
)
}
}
Loading

0 comments on commit 3725bc5

Please sign in to comment.