Skip to content

Commit

Permalink
Add a NonceCache interface to check nonces and prevent replay attacks
Browse files Browse the repository at this point in the history
fixes LIG-4117
  • Loading branch information
shreyav authored Jan 24, 2024
1 parent dfce5ed commit fcdd4d5
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 6 deletions.
67 changes: 65 additions & 2 deletions javatest/src/test/java/me/uma/javatest/UmaTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(),
"[email protected]",
"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(),
"[email protected]",
"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(),
"[email protected]",
"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) {
Expand Down
56 changes: 56 additions & 0 deletions uma-sdk/src/commonMain/kotlin/me/uma/NonceCache.kt
Original file line number Diff line number Diff line change
@@ -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<String, Long>()

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
}
}
35 changes: 31 additions & 4 deletions uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
)
}
Expand Down

0 comments on commit fcdd4d5

Please sign in to comment.