Skip to content

Commit

Permalink
make signature compatible with nodejs
Browse files Browse the repository at this point in the history
  • Loading branch information
gnarea committed Sep 16, 2023
1 parent a86eeab commit 1dc48b5
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package tech.relaycorp.letro.server.messages
import org.bouncycastle.asn1.DERBitString
import org.bouncycastle.asn1.DERUTF8String
import org.bouncycastle.asn1.DERVisibleString
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import tech.relaycorp.letro.utils.asn1.ASN1Utils
import tech.relaycorp.letro.utils.crypto.RSASigning
import tech.relaycorp.letro.utils.crypto.spkiEncode
import java.security.PrivateKey
import java.security.PublicKey
import java.util.Locale
Expand All @@ -19,16 +19,16 @@ class AccountRequest(
val requestEncoded = ASN1Utils.makeSequence(
listOf(
DERUTF8String(userName),
serialiseLocale(),
SubjectPublicKeyInfo.getInstance(veraidMemberPublicKey.encoded),
encodeLocale(),
veraidMemberPublicKey.spkiEncode(),
),
explicitTagging = false,
)
val signature = RSASigning.sign(requestEncoded.encoded, veraidMemberPrivateKey)
return ASN1Utils.serializeSequence(listOf(requestEncoded, DERBitString(signature)), false)
}

private fun serialiseLocale(): DERVisibleString {
private fun encodeLocale(): DERVisibleString {
val languageCode = locale.language.lowercase()
val countryCode = locale.country.lowercase()
val localeNormalised = if (languageCode.isEmpty()) {
Expand Down
41 changes: 41 additions & 0 deletions app/src/main/java/tech/relaycorp/letro/utils/crypto/KeyEncoding.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package tech.relaycorp.letro.utils.crypto

import org.bouncycastle.asn1.ASN1BitString
import org.bouncycastle.asn1.ASN1Integer
import org.bouncycastle.asn1.DERNull
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers
import org.bouncycastle.asn1.pkcs.RSASSAPSSparams
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import java.security.PublicKey

private val rsaPssSha256Mgf1Algorithm = AlgorithmIdentifier(
PKCSObjectIdentifiers.id_RSASSA_PSS,
RSASSAPSSparams(
AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256, DERNull.INSTANCE),
AlgorithmIdentifier(
PKCSObjectIdentifiers.id_mgf1,
AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256),
),
ASN1Integer(32),
ASN1Integer(1),
),
)

/**
* Encode the public key as a SubjectPublicKeyInfo structure with the RSA-PSS algorithm.
*
* Otherwise it'd be encoded as an RSA key (no padding specified), which isn't supported by
* the server (which uses the Node.js crypto module).
*/
fun PublicKey.spkiEncode(): SubjectPublicKeyInfo {
if (algorithm != "RSA") {
throw IllegalArgumentException("Only RSA keys are supported")
}

val keyWrapperEncoded = DERSequence.getInstance(encoded)
val keyEncoded = ASN1BitString.getInstance(keyWrapperEncoded.getObjectAt(1))
return SubjectPublicKeyInfo(rsaPssSha256Mgf1Algorithm, keyEncoded.bytes)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import org.junit.jupiter.api.Test
import tech.relaycorp.letro.testing.crypto.generateRSAKeyPair
import tech.relaycorp.letro.utils.asn1.ASN1Utils
import tech.relaycorp.letro.utils.crypto.RSASigning
import tech.relaycorp.letro.utils.crypto.spkiEncode
import java.util.Locale

const val USER_NAME = "alice"
Expand Down Expand Up @@ -163,7 +164,7 @@ class AccountRequestTest {
requestSequence.getObjectAt(2) as ASN1TaggedObject,
false,
)
publicKeyEncoded.encoded shouldBe keyPair.public.encoded
publicKeyEncoded shouldBe keyPair.public.spkiEncode()
}

private fun parseRequestSequence(serialisation: ByteArray): ASN1Sequence {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package tech.relaycorp.letro.utils.crypto

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
import org.bouncycastle.asn1.ASN1BitString
import org.bouncycastle.asn1.DERNull
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers
import org.bouncycastle.asn1.pkcs.RSASSAPSSparams
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import tech.relaycorp.letro.testing.crypto.generateRSAKeyPair
import java.security.KeyPairGenerator
import java.security.PublicKey

class KeyEncodingTest {
@Nested
inner class PublicKeySpkiEncoding {
val rsaPublicKey: PublicKey = generateRSAKeyPair().public

@Test
fun `Non-RSA keys should be refused`() {
val keyGen = KeyPairGenerator.getInstance("DSA", BC_PROVIDER)
keyGen.initialize(1024)
val keyPair = keyGen.generateKeyPair()

val exception = shouldThrow<IllegalArgumentException> {
keyPair.public.spkiEncode()
}

exception.message shouldBe "Only RSA keys are supported"
}

@Nested
inner class Algorithm {
@Test
fun `Algorithm should be RSA-PSS`() {
val encoding = rsaPublicKey.spkiEncode()

encoding.algorithm.algorithm shouldBe PKCSObjectIdentifiers.id_RSASSA_PSS
}

@Nested
inner class Params {
@Test
fun `Hash should be SHA-256`() {
val encoding = rsaPublicKey.spkiEncode()

val parameters = encoding.algorithm.parameters as RSASSAPSSparams
parameters.hashAlgorithm.algorithm shouldBe NISTObjectIdentifiers.id_sha256
parameters.hashAlgorithm.parameters shouldBe DERNull.INSTANCE
}

@Test
fun `MGF should be MGF1 with SHA-256`() {
val encoding = rsaPublicKey.spkiEncode()

val parameters = encoding.algorithm.parameters as RSASSAPSSparams
parameters.maskGenAlgorithm.algorithm shouldBe PKCSObjectIdentifiers.id_mgf1
val mgfAlgorithmParams = parameters.maskGenAlgorithm.parameters
mgfAlgorithmParams shouldBe AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256)
}

@Test
fun `Salt length should be 32`() {
val encoding = rsaPublicKey.spkiEncode()

val parameters = encoding.algorithm.parameters as RSASSAPSSparams
parameters.saltLength.intValueExact() shouldBe 32
}

@Test
fun `Trailer field should be 1`() {
val encoding = rsaPublicKey.spkiEncode()

val parameters = encoding.algorithm.parameters as RSASSAPSSparams
parameters.trailerField.intValueExact() shouldBe 1
}
}
}

@Test
fun `Key should be just the key without the algorithm`() {
val encoding = rsaPublicKey.spkiEncode()

val keyWrapperEncoded = DERSequence.getInstance(rsaPublicKey.encoded)
val keyEncoded = ASN1BitString.getInstance(keyWrapperEncoded.getObjectAt(1))
encoding.publicKeyData shouldBe keyEncoded
}
}
}

0 comments on commit 1dc48b5

Please sign in to comment.