Skip to content

Commit

Permalink
Ensure pre-key signed by identity key (#47)
Browse files Browse the repository at this point in the history
* add api client with grpc kotlin

* throw error if pre key is not signed by identity

* fix up the linter

* fix up signature verification

* fix up linter

* update readme to reflect maven central location

* readme update

* Update README.md

Co-authored-by: Jennifer Hasegawa <[email protected]>

* Update README.md

Co-authored-by: Jennifer Hasegawa <[email protected]>

---------

Co-authored-by: Jennifer Hasegawa <[email protected]>
  • Loading branch information
nplasterer and jhaaaa authored Mar 8, 2023
1 parent 6e68384 commit af919be
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 16 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ To learn more about XMTP and get answers to frequently asked questions, see [FAQ

For a basic demonstration of the core concepts and capabilities of the `xmtp-android` client SDK, see the [Example app project](https://github.com/xmtp/xmtp-android/tree/main/example). This is currently a work in progress.

## Install from the GitHub Packages
## Install from Maven Central

While in Pre-Preview status, we plan to [release in GitHub Packages](https://github.com/xmtp/xmtp-android/packages/1797061). When this moves to Dev Preview status, we will have this released in Maven Central. For help consuming GitHub Packages, read [this doc](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#using-a-published-package).
You can find the latest package version on [Maven Central](https://central.sonatype.com/artifact/org.xmtp/android/0.0.4)

```gradle
implementation 'org.xmtp:android:X.X.X'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,23 @@ class MessageV2Builder {
val decrypted =
Crypto.decrypt(keyMaterial, message.ciphertext, message.headerBytes.toByteArray())
val signed = SignedContent.parseFrom(decrypted)

if (!signed.sender.hasPreKey() || !signed.sender.hasIdentityKey()) {
throw XMTPException("missing sender pre-key or identity key")
}

val senderPreKey = PublicKeyBuilder.buildFromSignedPublicKey(signed.sender.preKey)
val senderIdentityKey =
PublicKeyBuilder.buildFromSignedPublicKey(signed.sender.identityKey)

if (!senderPreKey.signature.verify(
senderIdentityKey,
signed.sender.preKey.keyBytes.toByteArray()
)
) {
throw XMTPException("pre-key not signed by identity key")
}

// Verify content signature
val digest =
Hash.sha256(message.headerBytes.toByteArray() + signed.payload.toByteArray())
Expand Down Expand Up @@ -69,9 +86,9 @@ class MessageV2Builder {
val header = MessageHeaderV2Builder.buildFromTopic(topic, date)
val headerBytes = header.toByteArray()
val digest = Hash.sha256(headerBytes + payload)
val preKey = client.keys?.preKeysList?.get(0)
val preKey = client.keys.preKeysList?.get(0)
val signature = preKey?.sign(digest)
val bundle = client.privateKeyBundleV1?.toV2()?.getPublicKeyBundle()
val bundle = client.privateKeyBundleV1.toV2().getPublicKeyBundle()
val signedContent = SignedContentBuilder.builderFromPayload(payload, bundle, signature)
val signedBytes = signedContent.toByteArray()
val ciphertext = Crypto.encrypt(keyMaterial, signedBytes, additionalData = headerBytes)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package org.xmtp.android.library.messages

import org.web3j.crypto.ECDSASignature
import org.web3j.crypto.Sign
import org.xmtp.android.library.KeyUtil
import org.bouncycastle.jce.ECNamedCurveTable
import org.bouncycastle.jce.ECPointUtil
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec
import org.bouncycastle.jce.spec.ECNamedCurveSpec
import org.bouncycastle.util.Arrays
import org.xmtp.android.library.Util
import org.xmtp.android.library.toHex
import org.xmtp.proto.message.contents.SignatureOuterClass
import java.math.BigInteger
import java.security.KeyFactory
import java.security.interfaces.ECPublicKey
import java.security.spec.ECPublicKeySpec

typealias Signature = org.xmtp.proto.message.contents.SignatureOuterClass.Signature

Expand Down Expand Up @@ -38,14 +44,43 @@ val Signature.rawDataWithNormalizedRecovery: ByteArray
}

fun Signature.verify(signedBy: PublicKey, digest: ByteArray): Boolean {
val signatureData = KeyUtil.getSignatureData(ecdsaCompact.bytes.toByteArray() + ecdsaCompact.recovery.toByte())
val publicKey = Sign.recoverFromSignature(
BigInteger(1, signatureData.v).toInt(),
ECDSASignature(BigInteger(1, signatureData.r), BigInteger(1, signatureData.s)),
digest,
)
val pubKey = KeyUtil.addUncompressedByte(publicKey.toByteArray())
return pubKey.contentEquals(signedBy.secp256K1Uncompressed.bytes.toByteArray())
val ecdsaVerify = java.security.Signature.getInstance("SHA256withECDSA", BouncyCastleProvider())
ecdsaVerify.initVerify(getPublicKeyFromBytes(signedBy.secp256K1Uncompressed.bytes.toByteArray()))
ecdsaVerify.update(digest)
return ecdsaVerify.verify(normalizeSignatureForVerification(this.rawDataWithNormalizedRecovery))
}

private fun normalizeSignatureForVerification(signature: ByteArray): ByteArray {
val r: ByteArray = BigInteger(1, Arrays.copyOfRange(signature, 0, 32)).toByteArray()
val s: ByteArray = BigInteger(1, Arrays.copyOfRange(signature, 32, 64)).toByteArray()
val der = ByteArray(6 + r.size + s.size)
der[0] = 0x30 // Tag of signature object

der[1] = (der.size - 2).toByte() // Length of signature object

var o = 2
der[o++] = 0x02 // Tag of ASN1 Integer

der[o++] = r.size.toByte() // Length of first signature part

System.arraycopy(r, 0, der, o, r.size)
o += r.size
der[o++] = 0x02 // Tag of ASN1 Integer

der[o++] = s.size.toByte() // Length of second signature part

System.arraycopy(s, 0, der, o, s.size)

return der
}

private fun getPublicKeyFromBytes(pubKey: ByteArray): java.security.PublicKey {
val spec: ECNamedCurveParameterSpec = ECNamedCurveTable.getParameterSpec("secp256k1")
val kf: KeyFactory = KeyFactory.getInstance("ECDSA", BouncyCastleProvider())
val params = ECNamedCurveSpec("secp256k1", spec.curve, spec.g, spec.n)
val point = ECPointUtil.decodePoint(params.curve, pubKey)
val pubKeySpec = ECPublicKeySpec(point, params)
return kf.generatePublic(pubKeySpec) as ECPublicKey
}

fun Signature.ensureWalletSignature(): Signature {
Expand Down
27 changes: 27 additions & 0 deletions library/src/test/java/org/xmtp/android/library/ConversationTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import org.xmtp.android.library.messages.toSignedPublicKeyBundle
import org.xmtp.android.library.messages.toV2
import org.xmtp.android.library.messages.walletAddress
import org.xmtp.proto.message.contents.Invitation
import org.xmtp.proto.message.contents.Invitation.InvitationV1.Context
import java.nio.charset.StandardCharsets
import java.util.Date

Expand Down Expand Up @@ -517,4 +518,30 @@ class ConversationTest {
awaitComplete()
}
}

@Test
fun testV2RejectsSpoofedContactBundles() {
val topic = "/xmtp/0/m-Gdb7oj5nNdfZ3MJFLAcS4WTABgr6al1hePy6JV1-QUE/proto"
val envelopeMessage =
com.google.crypto.tink.subtle.Base64.decode("Er0ECkcIwNruhKLgkKUXEjsveG10cC8wL20tR2RiN29qNW5OZGZaM01KRkxBY1M0V1RBQmdyNmFsMWhlUHk2SlYxLVFVRS9wcm90bxLxAwruAwognstLoG6LWgiBRsWuBOt+tYNJz+CqCj9zq6hYymLoak8SDFsVSy+cVAII0/r3sxq7A/GCOrVtKH6J+4ggfUuI5lDkFPJ8G5DHlysCfRyFMcQDIG/2SFUqSILAlpTNbeTC9eSI2hUjcnlpH9+ncFcBu8StGfmilVGfiADru2fGdThiQ+VYturqLIJQXCHO2DkvbbUOg9xI66E4Hj41R9vE8yRGeZ/eRGRLRm06HftwSQgzAYf2AukbvjNx/k+xCMqti49Qtv9AjzxVnwttLiA/9O+GDcOsiB1RQzbZZzaDjQ/nLDTF6K4vKI4rS9QwzTJqnoCdp0SbMZFf+KVZpq3VWnMGkMxLW5Fr6gMvKny1e1LAtUJSIclI/1xPXu5nsKd4IyzGb2ZQFXFQ/BVL9Z4CeOZTsjZLGTOGS75xzzGHDtKohcl79+0lgIhAuSWSLDa2+o2OYT0fAjChp+qqxXcisAyrD5FB6c9spXKfoDZsqMV/bnCg3+udIuNtk7zBk7jdTDMkofEtE3hyIm8d3ycmxKYOakDPqeo+Nk1hQ0ogxI8Z7cEoS2ovi9+rGBMwREzltUkTVR3BKvgV2EOADxxTWo7y8WRwWxQ+O6mYPACsiFNqjX5Nvah5lRjihphQldJfyVOG8Rgf4UwkFxmI")
val keyMaterial =
com.google.crypto.tink.subtle.Base64.decode("R0BBM5OPftNEuavH/991IKyJ1UqsgdEG4SrdxlIG2ZY=")

val conversation = ConversationV2(
topic = topic,
keyMaterial = keyMaterial,
context = Context.newBuilder().build(),
peerAddress = "0x2f25e33D7146602Ec08D43c1D6B1b65fc151A677",
client = aliceClient,
header = Invitation.SealedInvitationHeaderV1.newBuilder().build()
)
val envelope = EnvelopeBuilder.buildFromString(
topic = topic,
timestamp = Date(),
message = envelopeMessage
)
assertThrows("pre-key not signed by identity key", XMTPException::class.java) {
conversation.decodeEnvelope(envelope)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ package org.xmtp.android.library

import com.google.protobuf.kotlin.toByteStringUtf8
import org.junit.Test
import org.web3j.crypto.Hash
import org.xmtp.android.library.messages.PrivateKeyBuilder
import org.xmtp.android.library.messages.verify

class SignatureTest {
@Test
fun testVerify() {
val digest = "Hello world".toByteStringUtf8().toByteArray()
val digest = Hash.sha256("Hello world".toByteStringUtf8().toByteArray())
val signingKey = PrivateKeyBuilder()
val signature = signingKey.sign(digest)
assert(
Expand Down

0 comments on commit af919be

Please sign in to comment.