-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement account creation message deserialisation
- Loading branch information
Showing
7 changed files
with
465 additions
and
94 deletions.
There are no files selected for viewing
93 changes: 93 additions & 0 deletions
93
app/src/main/java/tech/relaycorp/letro/server/messages/AccountCreation.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 4 additions & 0 deletions
4
app/src/main/java/tech/relaycorp/letro/server/messages/MalformedAccountCreationException.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
23
app/src/main/java/tech/relaycorp/letro/utils/i18n/Locale.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
212 changes: 212 additions & 0 deletions
212
app/src/test/java/tech/relaycorp/letro/server/messages/AccountCreationTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) | ||
} | ||
} |
Oops, something went wrong.