Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
shreyav committed Mar 6, 2024
1 parent 809efed commit f458e3b
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 36 deletions.
29 changes: 29 additions & 0 deletions javatest/src/test/java/me/uma/javatest/UmaTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ public class UmaTest {
private static final String PUBKEY_HEX = "04f2998ab056897ddb91b5e6fad1e4bb6c4b7dda427409f667d0f4694b553e4feeeb08936c2993f7b931f6a3fa7e846f11165fae222de5e4a55c12def21a7c9fcf";
private static final String PRIVKEY_HEX = "10fbbee8f689b207bb22df2dfa27827ae9ae02e265980ea09ef5101ed5fb508f";

private static final String CERT = "-----BEGIN CERTIFICATE-----\n" +
"MIIB1zCCAXygAwIBAgIUGN3ihBj1RnKoeTM/auDFnNoThR4wCgYIKoZIzj0EAwIw\n" +
"QjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCmNhbGlmb3JuaWExDjAMBgNVBAcMBWxv\n" +
"cyBhMQ4wDAYDVQQKDAVsaWdodDAeFw0yNDAzMDUyMTAzMTJaFw0yNDAzMTkyMTAz\n" +
"MTJaMEIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApjYWxpZm9ybmlhMQ4wDAYDVQQH\n" +
"DAVsb3MgYTEOMAwGA1UECgwFbGlnaHQwVjAQBgcqhkjOPQIBBgUrgQQACgNCAARB\n" +
"nFRn6lY/ABD9YU+F6IWsmcIbjo1BYkEXX91e/SJE/pB+Lm+j3WYxsbF80oeY2o2I\n" +
"KjTEd21EzECQeBx6reobo1MwUTAdBgNVHQ4EFgQUU87LnQdiP6XIE6LoKU1PZnbt\n" +
"bMwwHwYDVR0jBBgwFoAUU87LnQdiP6XIE6LoKU1PZnbtbMwwDwYDVR0TAQH/BAUw\n" +
"AwEB/zAKBggqhkjOPQQDAgNJADBGAiEAvsrvoeo3rbgZdTHxEUIgP0ArLyiO34oz\n" +
"NlwL4gk5GpgCIQCvRx4PAyXNV9T6RRE+3wFlqwluOc/pPOjgdRw/wpoNPQ==\n" +
"-----END CERTIFICATE-----";

@Test
public void testFetchPublicKeySync() throws Exception {
Expand Down Expand Up @@ -252,6 +264,23 @@ callback, new PubKeyResponse(publicKeyBytes(), publicKeyBytes()),
new InMemoryNonceCache(1L)));
}

@Test
public void serializeAndDeserializePubKeyResponse() {
PubKeyResponse keysOnlyResponse =
new PubKeyResponse(UmaTest.hexToBytes("02d5fe"), UmaTest.hexToBytes("123456"));
String json = keysOnlyResponse.toJson();
PubKeyResponse parsedResponse = umaProtocolHelper.parseAsPubKeyResponse(json);
assertNotNull(parsedResponse);
assertEquals(keysOnlyResponse, parsedResponse);

PubKeyResponse certsOnlyResponse =
new PubKeyResponse(CERT, CERT);
json = certsOnlyResponse.toJson();
parsedResponse = umaProtocolHelper.parseAsPubKeyResponse(json);
assertNotNull(parsedResponse);
assertEquals(certsOnlyResponse, parsedResponse);
}

static byte[] hexToBytes(String hex) {
byte[] bytes = new byte[hex.length() / 2];
for (int i = 0; i < hex.length(); i += 2) {
Expand Down
14 changes: 9 additions & 5 deletions uma-sdk/src/commonMain/kotlin/me/uma/PublicKeyCache.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,20 @@ class InMemoryPublicKeyCache : PublicKeyCache {

override fun getPublicKeysForVasp(vaspDomain: String): PubKeyResponse? {
val pubKeyResponse = cache[vaspDomain]
return if (pubKeyResponse?.expirationTimestamp == null
|| pubKeyResponse.expirationTimestamp < System.currentTimeMillis() / 1000) {
return if (pubKeyResponse?.expirationTimestamp == null ||
pubKeyResponse.expirationTimestamp < System.currentTimeMillis() / 1000
) {
cache.remove(vaspDomain)
null
} else pubKeyResponse
} else {
pubKeyResponse
}
}

override fun addPublicKeysForVasp(vaspDomain: String, pubKeyResponse: PubKeyResponse) {
if (pubKeyResponse.expirationTimestamp != null
&& pubKeyResponse.expirationTimestamp > System.currentTimeMillis() / 1000) {
if (pubKeyResponse.expirationTimestamp != null &&
pubKeyResponse.expirationTimestamp > System.currentTimeMillis() / 1000
) {
cache[vaspDomain] = pubKeyResponse
}
}
Expand Down
6 changes: 5 additions & 1 deletion uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,15 @@ class UmaProtocolHelper @JvmOverloads constructor(

val scheme = if (isDomainLocalhost(vaspDomain)) "http" else "https"
val response = umaRequester.makeGetRequest("$scheme://$vaspDomain/.well-known/lnurlpubkey")
val pubKeyResponse = Json.decodeFromString<PubKeyResponse>(response)
val pubKeyResponse = parseAsPubKeyResponse(response)
publicKeyCache.addPublicKeysForVasp(vaspDomain, pubKeyResponse)
return pubKeyResponse
}

fun parseAsPubKeyResponse(response: String): PubKeyResponse {
return Json.decodeFromString(response)
}

private fun generateNonce(): String {
return Random.nextULong().toString()
}
Expand Down
62 changes: 32 additions & 30 deletions uma-sdk/src/commonMain/kotlin/me/uma/protocol/PubKeyResponse.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import java.security.interfaces.ECPublicKey
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import me.uma.crypto.hexToByteArray
import me.uma.utils.ByteArrayAsHexSerializer
import me.uma.utils.X509CertificateSerializer

/**
* Response from another VASP when requesting public keys.
Expand All @@ -23,60 +23,50 @@ import me.uma.utils.ByteArrayAsHexSerializer
*/
@Serializable
data class PubKeyResponse internal constructor(
private val signingCertificate: String? = null,
private val encryptionCertificate: String? = null,
@Serializable(with = X509CertificateSerializer::class)
val signingCertificate: X509Certificate?,
@Serializable(with = X509CertificateSerializer::class)
val encryptionCertificate: X509Certificate?,
@Serializable(with = ByteArrayAsHexSerializer::class)
private val signingPubKey: ByteArray? = null,
private val signingPubKey: ByteArray?,
@Serializable(with = ByteArrayAsHexSerializer::class)
private val encryptionPubKey: ByteArray? = null,
val expirationTimestamp: Long?,
private val encryptionPubKey: ByteArray?,
val expirationTimestamp: Long? = null,
) {
@JvmOverloads
constructor(signingKey: ByteArray, encryptionKey: ByteArray, expirationTs: Long? = null) : this(
signingCertificate = null,
encryptionCertificate = null,
signingPubKey = signingKey,
encryptionPubKey = encryptionKey,
expirationTimestamp = expirationTs,
)

@JvmOverloads
constructor(signingCert: String, encryptionCert: String, expirationTs: Long? = null) : this(
signingCertificate = signingCert,
encryptionCertificate = encryptionCert,
signingCertificate = signingCert.toX509Certificate(),
encryptionCertificate = encryptionCert.toX509Certificate(),
signingPubKey = signingCert.toX509Certificate().getPubKeyBytes(),
encryptionPubKey = encryptionCert.toX509Certificate().getPubKeyBytes(),
expirationTimestamp = expirationTs,
)

fun getSigningPubKey(): ByteArray {
val signingCertificate = getSigningCertificate()
return if (signingCertificate != null) {
(signingCertificate.publicKey as ECPublicKey).toHexByteArray()
signingCertificate.getPubKeyBytes()
} else {
signingPubKey ?: throw IllegalStateException("No signing public key")
}
}

fun getEncryptionPubKey(): ByteArray {
val encryptionCertificate = getEncryptionCertificate()
return if (encryptionCertificate != null) {
(encryptionCertificate.publicKey as ECPublicKey).toHexByteArray()
encryptionCertificate.getPubKeyBytes()
} else {
encryptionPubKey ?: throw IllegalStateException("No encryption public key")
}
}

fun getSigningCertificate(): X509Certificate? {
return signingCertificate?.let {
CertificateFactory.getInstance("X509")
.generateCertificate(signingCertificate.byteInputStream()) as X509Certificate
}
}

fun getEncryptionCertificate(): X509Certificate? {
return encryptionCertificate?.let {
CertificateFactory.getInstance("X509")
.generateCertificate(encryptionCertificate.byteInputStream()) as X509Certificate
}
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
Expand All @@ -103,8 +93,20 @@ data class PubKeyResponse internal constructor(

fun toJson() = Json.encodeToString(this)
}
fun ECPublicKey.toHexByteArray(): ByteArray {
val xHex = this.w.affineX.toString(16)
val yHex = this.w.affineY.toString(16)
return "04$xHex$yHex".hexToByteArray()

private fun String.toX509Certificate(): X509Certificate {
return CertificateFactory.getInstance("X.509")
.generateCertificate(byteInputStream()) as? X509Certificate
?: throw IllegalStateException("Could not be parsed as X.509 certificate")
}

private fun X509Certificate.getPubKeyBytes(): ByteArray {
if (publicKey !is ECPublicKey ||
!(publicKey as ECPublicKey).params.toString().contains("secp256k1")
) {
throw IllegalStateException("Public key extracted from certificate is not EC/secp256k1")
}
// encryptionPubKey.publicKey is an ASN.1/DER encoded X.509/SPKI key, the last 65
// bytes are the uncompressed public key
return publicKey.encoded.takeLast(65).toByteArray()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package me.uma.utils

import java.io.ByteArrayInputStream
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

@OptIn(ExperimentalStdlibApi::class)
class X509CertificateSerializer : KSerializer<X509Certificate> {
override val descriptor = buildClassSerialDescriptor("X509Certificate")

override fun serialize(encoder: Encoder, value: X509Certificate) {
encoder.encodeString(value.encoded.toHexString())
}

override fun deserialize(decoder: Decoder): X509Certificate {
val bytes = decoder.decodeString().hexToByteArray()
return CertificateFactory.getInstance("X.509")
.generateCertificate(ByteArrayInputStream(bytes)) as X509Certificate
}
}

0 comments on commit f458e3b

Please sign in to comment.