From fcdd4d57bd06519f37275b117b25c4dbc5f02628 Mon Sep 17 00:00:00 2001 From: Shreya Vissamsetti Date: Tue, 23 Jan 2024 17:25:48 -0800 Subject: [PATCH] Add a NonceCache interface to check nonces and prevent replay attacks fixes LIG-4117 --- .../test/java/me/uma/javatest/UmaTest.java | 67 ++++++++++++++++++- .../commonMain/kotlin/me/uma/NonceCache.kt | 56 ++++++++++++++++ .../kotlin/me/uma/UmaProtocolHelper.kt | 35 ++++++++-- 3 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 uma-sdk/src/commonMain/kotlin/me/uma/NonceCache.kt diff --git a/javatest/src/test/java/me/uma/javatest/UmaTest.java b/javatest/src/test/java/me/uma/javatest/UmaTest.java index fb9058f..0c46ba6 100644 --- a/javatest/src/test/java/me/uma/javatest/UmaTest.java +++ b/javatest/src/test/java/me/uma/javatest/UmaTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import org.jetbrains.annotations.NotNull; @@ -12,7 +13,9 @@ import java.util.concurrent.CompletableFuture; import kotlin.coroutines.Continuation; +import me.uma.InMemoryNonceCache; import me.uma.InMemoryPublicKeyCache; +import me.uma.InvalidNonceException; import me.uma.SyncUmaInvoiceCreator; import me.uma.UmaInvoiceCreator; import me.uma.UmaProtocolHelper; @@ -59,7 +62,9 @@ public void testGetLnurlpRequest() throws Exception { System.out.println(lnurlpUrl); LnurlpRequest request = umaProtocolHelper.parseLnurlpRequest(lnurlpUrl); assertNotNull(request); - assertTrue(umaProtocolHelper.verifyUmaLnurlpQuerySignature(request, new PubKeyResponse(publicKeyBytes(), publicKeyBytes()))); + assertTrue(umaProtocolHelper.verifyUmaLnurlpQuerySignature( + request, new PubKeyResponse(publicKeyBytes(), publicKeyBytes()), + new InMemoryNonceCache(1L))); System.out.println(request); } @@ -102,7 +107,8 @@ public void testGetLnurlpResponse() throws Exception { assertNotNull(parsedResponse); assertEquals(lnurlpResponse, parsedResponse); assertTrue(umaProtocolHelper.verifyLnurlpResponseSignature( - parsedResponse, new PubKeyResponse(publicKeyBytes(), publicKeyBytes()))); + parsedResponse, new PubKeyResponse(publicKeyBytes(), publicKeyBytes()), + new InMemoryNonceCache(1L))); } @Test @@ -153,6 +159,63 @@ public void testGetPayReqResponseFuture() throws Exception { System.out.println(response); } + @Test + public void testVerifyUmaLnurlpQuerySignature_duplicateNonce() throws Exception { + String lnurlpUrl = umaProtocolHelper.getSignedLnurlpRequestUrl( + privateKeyBytes(), + "$bob@vasp2.com", + "https://vasp.com", + true); + LnurlpRequest request = umaProtocolHelper.parseLnurlpRequest(lnurlpUrl); + assertNotNull(request); + + InMemoryNonceCache nonceCache = new InMemoryNonceCache(1L); + nonceCache.checkAndSaveNonce(request.getNonce(), 2L); + + Exception exception = assertThrows(InvalidNonceException.class, () -> { + umaProtocolHelper.verifyUmaLnurlpQuerySignature( + request, new PubKeyResponse(publicKeyBytes(), publicKeyBytes()), nonceCache); + }); + assertEquals("Nonce already used", exception.getMessage()); + } + + @Test + public void testVerifyUmaLnurlpQuerySignature_oldSignature() throws Exception { + String lnurlpUrl = umaProtocolHelper.getSignedLnurlpRequestUrl( + privateKeyBytes(), + "$bob@vasp2.com", + "https://vasp.com", + true); + LnurlpRequest request = umaProtocolHelper.parseLnurlpRequest(lnurlpUrl); + assertNotNull(request); + + InMemoryNonceCache nonceCache = new InMemoryNonceCache(System.currentTimeMillis() / 1000 + 1000); + + Exception exception = assertThrows(InvalidNonceException.class, () -> { + umaProtocolHelper.verifyUmaLnurlpQuerySignature( + request, new PubKeyResponse(publicKeyBytes(), publicKeyBytes()), nonceCache); + }); + assertEquals("Timestamp too old", exception.getMessage()); + } + + @Test + public void testVerifyUmaLnurlpQuerySignature_purgeOlderNoncesAndStoreNonce() throws Exception { + String lnurlpUrl = umaProtocolHelper.getSignedLnurlpRequestUrl( + privateKeyBytes(), + "$bob@vasp2.com", + "https://vasp.com", + true); + LnurlpRequest request = umaProtocolHelper.parseLnurlpRequest(lnurlpUrl); + assertNotNull(request); + + InMemoryNonceCache nonceCache = new InMemoryNonceCache(1L); + nonceCache.checkAndSaveNonce(request.getNonce(), 2L); + nonceCache.purgeNoncesOlderThan(3L); + + assertTrue(umaProtocolHelper.verifyUmaLnurlpQuerySignature( + request, new PubKeyResponse(publicKeyBytes(), publicKeyBytes()), nonceCache)); + } + static byte[] hexToBytes(String hex) { byte[] bytes = new byte[hex.length() / 2]; for (int i = 0; i < hex.length(); i += 2) { diff --git a/uma-sdk/src/commonMain/kotlin/me/uma/NonceCache.kt b/uma-sdk/src/commonMain/kotlin/me/uma/NonceCache.kt new file mode 100644 index 0000000..d1a9236 --- /dev/null +++ b/uma-sdk/src/commonMain/kotlin/me/uma/NonceCache.kt @@ -0,0 +1,56 @@ +package me.uma + +/** + * An interface for caching of nonces used in signatures. This is used to prevent replay attacks. + * Implementations of this interface should be thread-safe. + */ +interface NonceCache { + /** + * Checks if the given nonce has been used before, and if not, saves it. + * If the nonce has been used before, or if timestamp is too old, throws [InvalidNonceException]. + * + * @param nonce The nonce to cache. + * @param timestamp Timestamp corresponding to the nonce in seconds since epoch. + */ + fun checkAndSaveNonce(nonce: String, timestamp: Long) + + /** + * Purges all nonces older than the given timestamp. This allows the cache to be pruned. + * + * @param timestamp The timestamp before which nonces should be removed. + */ + fun purgeNoncesOlderThan(timestamp: Long) +} + +class InvalidNonceException(message: String) : Exception(message) + +/** + * InMemoryNonceCache is an in-memory implementation of NonceCache. + * It is not recommended to use this in production, as it will not persist across restarts. You likely want to implement + * your own NonceCache that persists to a database of some sort. + */ +class InMemoryNonceCache(private var oldestValidTimestamp: Long) : NonceCache { + private val cache = mutableMapOf() + + override fun checkAndSaveNonce(nonce: String, timestamp: Long) { + if (timestamp < oldestValidTimestamp) { + throw InvalidNonceException("Timestamp too old") + } + if (cache.contains(nonce)) { + throw InvalidNonceException("Nonce already used") + } else { + cache[nonce] = timestamp + } + } + + override fun purgeNoncesOlderThan(timestamp: Long) { + val iterator = cache.entries.iterator() + while (iterator.hasNext()) { + val entry = iterator.next() + if (entry.value < timestamp) { + iterator.remove() + } + } + oldestValidTimestamp = timestamp + } +} diff --git a/uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt b/uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt index bddb1f3..7dc4c1e 100644 --- a/uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt +++ b/uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt @@ -140,8 +140,19 @@ class UmaProtocolHelper @JvmOverloads constructor( /** * Verifies the signature on an UMA Lnurlp query based on the public key of the VASP making the request. + * + * @param query The signed [LnurlpRequest] to verify. + * @param pubKeyResponse The [PubKeyResponse] that contains the public key of the receiver. + * @return true if the signature is valid, false otherwise. + * @throws InvalidNonceException if the nonce has already been used/timestamp is too old. */ - fun verifyUmaLnurlpQuerySignature(query: LnurlpRequest, pubKeyResponse: PubKeyResponse): Boolean { + @Throws(InvalidNonceException::class) + fun verifyUmaLnurlpQuerySignature( + query: LnurlpRequest, + pubKeyResponse: PubKeyResponse, + nonceCache: NonceCache, + ): Boolean { + nonceCache.checkAndSaveNonce(query.nonce, query.timestamp) return verifySignature(query.signablePayload(), query.signature, pubKeyResponse.signingPubKey) } @@ -219,8 +230,16 @@ class UmaProtocolHelper @JvmOverloads constructor( * * @param response The signed [LnurlpResponse] sent by the receiver. * @param pubKeyResponse The [PubKeyResponse] that contains the public key of the receiver. + * @return true if the signature is valid, false otherwise. + * @throws InvalidNonceException if the nonce has already been used/timestamp is too old. */ - fun verifyLnurlpResponseSignature(response: LnurlpResponse, pubKeyResponse: PubKeyResponse): Boolean { + @Throws(InvalidNonceException::class) + fun verifyLnurlpResponseSignature( + response: LnurlpResponse, + pubKeyResponse: PubKeyResponse, + nonceCache: NonceCache, + ): Boolean { + nonceCache.checkAndSaveNonce(response.compliance.signatureNonce, response.compliance.signatureTimestamp) return verifySignature( response.compliance.signablePayload(), response.compliance.signature, @@ -343,11 +362,19 @@ class UmaProtocolHelper @JvmOverloads constructor( * @param payReq The [PayRequest] sent by the sender. * @param pubKeyResponse The [PubKeyResponse] that contains the public key of the sender. * @return true if the signature is valid, false otherwise. + * @throws InvalidNonceException if the nonce has already been used/timestamp is too old. */ - fun verifyPayReqSignature(payReq: PayRequest, pubKeyResponse: PubKeyResponse): Boolean { + @Throws(InvalidNonceException::class) + fun verifyPayReqSignature( + payReq: PayRequest, + pubKeyResponse: PubKeyResponse, + nonceCache: NonceCache, + ): Boolean { + val compliance = payReq.payerData.compliance ?: return false + nonceCache.checkAndSaveNonce(compliance.signatureNonce, compliance.signatureTimestamp) return verifySignature( payReq.signablePayload(), - payReq.payerData.compliance!!.signature, + compliance.signature, pubKeyResponse.signingPubKey, ) }