Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backing signature support #64

Merged
merged 2 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 167 additions & 1 deletion javatest/src/test/java/me/uma/javatest/UmaTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
import static java.util.Objects.requireNonNull;
import static org.junit.jupiter.api.Assertions.*;
import static me.uma.protocol.CurrencyUtils.createCurrency;
import static me.uma.utils.SerializationKt.getSerialFormat;

public class UmaTest {
UmaProtocolHelper umaProtocolHelper = new UmaProtocolHelper(new InMemoryPublicKeyCache(), new TestUmaRequester());
PublicKeyCache publicKeyCache = new InMemoryPublicKeyCache();
UmaProtocolHelper umaProtocolHelper = new UmaProtocolHelper(publicKeyCache, new TestUmaRequester());
private static final String PUBKEY_HEX = "04419c5467ea563f0010fd614f85e885ac99c21b8e8d416241175fdd5efd2244fe907e2e6fa3dd6631b1b17cd28798da8d882a34c4776d44cc4090781c7aadea1b";
private static final String PRIVKEY_HEX = "77e891f0ecd265a3cda435eaa73792233ebd413aeb0dbb66f2940babfc9a2667";
private static final String encodedPayReqMetadata = "[[\"text/uma-invoice\",\"invoiceUUID\"],[\"text/plain\",\"otherInformations\"]]";
Expand Down Expand Up @@ -197,6 +199,63 @@ public void testGetLnurlpResponse_umaV0() throws Exception {
new InMemoryNonceCache(1L)));
}

@Test
public void testSignAndVerifyLnurlpResponseWithBackingSignature() throws Exception {
String lnurlpUrl = umaProtocolHelper.getSignedLnurlpRequestUrl(
privateKeyBytes(),
"[email protected]",
"https://vasp.com",
true);
LnurlpRequest request = umaProtocolHelper.parseLnurlpRequest(lnurlpUrl);
assertNotNull(request);
LnurlpResponse lnurlpResponse = umaProtocolHelper.getLnurlpResponse(
request,
privateKeyBytes(),
true,
"https://vasp2.com/callback",
"encoded metadata",
1,
10_000_000,
CounterPartyData.createCounterPartyDataOptions(
Map.of(
"name", false,
"email", false,
"identity", true,
"compliance", true
)
),
List.of(
createCurrency(
"USD",
"US Dollar",
"$",
34_150,
2,
1,
10_000_000,
"0.3"
)
),
KycStatus.VERIFIED
);
assertNotNull(lnurlpResponse);
String backingDomain = "backingvasp.com";
UmaLnurlpResponse umaResponse = lnurlpResponse.asUmaResponse();
assertNotNull(umaResponse);
UmaLnurlpResponse responseWithBackingSignature = umaResponse.appendBackingSignature(privateKeyBytes(), backingDomain);
String responseJson = responseWithBackingSignature.toJson();
LnurlpResponse parsedResponse = umaProtocolHelper.parseAsLnurlpResponse(responseJson);
assertNotNull(parsedResponse);
UmaLnurlpResponse parsedUmaResponse = parsedResponse.asUmaResponse();
assertNotNull(parsedUmaResponse);
assertNotNull(parsedUmaResponse.getBackingSignatures());
assertEquals(1, parsedUmaResponse.getBackingSignatures().size());
Long publicKeyCacheExpiry = System.currentTimeMillis() / 1000 + 10000;
PubKeyResponse backingVaspPubKeyResponse = new PubKeyResponse(publicKeyBytes(), publicKeyBytes(), publicKeyCacheExpiry);
publicKeyCache.addPublicKeysForVasp(backingDomain, backingVaspPubKeyResponse);
assertTrue(umaProtocolHelper.verifyLnurlpResponseBackingSignaturesSync(parsedUmaResponse));
}

@Test
public void testGetPayRequest_umaV1() throws Exception {
PayRequest request = umaProtocolHelper.getPayRequest(
Expand Down Expand Up @@ -263,6 +322,38 @@ request, new PubKeyResponse(publicKeyBytes(), publicKeyBytes()),
assertEquals(request, parsedRequest);
}

@Test
public void testSignAndVerifyPayReqBackingSignatures() throws Exception {
PayRequest request = umaProtocolHelper.getPayRequest(
publicKeyBytes(),
privateKeyBytes(),
"USD",
1000L,
true,
"[email protected]",
KycStatus.VERIFIED,
"/api/lnurl/utxocallback?txid=1234"
);
String backingDomain = "backingvasp.com";
PayRequest requestWithBackingSignatures = request.appendBackingSignature(privateKeyBytes(), backingDomain);
String requestJson = requestWithBackingSignatures.toJson();
PayRequest parsedRequest = umaProtocolHelper.parseAsPayRequest(requestJson);
assertNotNull(parsedRequest);
JsonObject payerData = parsedRequest.getPayerData();
assertNotNull(payerData);
CompliancePayerData complianceData = getSerialFormat().decodeFromJsonElement(
CompliancePayerData.Companion.serializer(),
payerData.get("compliance")
);
assertNotNull(complianceData);
assertNotNull(complianceData.getBackingSignatures());
assertEquals(1, complianceData.getBackingSignatures().size());
Long publicKeyCacheExpiry = System.currentTimeMillis() / 1000 + 10000;
PubKeyResponse backingVaspPubKeyResponse = new PubKeyResponse(publicKeyBytes(), publicKeyBytes(), publicKeyCacheExpiry);
publicKeyCache.addPublicKeysForVasp(backingDomain, backingVaspPubKeyResponse);
assertTrue(umaProtocolHelper.verifyPayReqBackingSignaturesSync(parsedRequest));
}

@Test
public void testGetPayReqResponseSync_umaV1() throws Exception {
PayRequest request = umaProtocolHelper.getPayRequest(
Expand Down Expand Up @@ -348,6 +439,55 @@ response, new PubKeyResponse(publicKeyBytes(), publicKeyBytes()),
assertEquals(response, parsedResponse);
}

@Test
public void testSignAndVerifyPayReqResponseBackingSignatures() throws Exception {
PayRequest request = umaProtocolHelper.getPayRequest(
publicKeyBytes(),
privateKeyBytes(),
"USD",
100L,
true,
"[email protected]",
KycStatus.VERIFIED,
"/api/lnurl/utxocallback?txid=1234"
);
PayReqResponse response = umaProtocolHelper.getPayReqResponseSync(
request,
new TestSyncUmaInvoiceCreator(),
encodedPayReqMetadata,
"USD",
2,
24150.0,
100000L,
List.of("abcdef12345"),
null,
"/api/lnurl/utxocallback?txid=1234",
privateKeyBytes(),
PayeeData.createPayeeData(null, "[email protected]"),
null,
null,
"1.0"
);
String backingDomain = "backingvasp.com";
PayReqResponse signedResponse = response.appendBackingSignature(privateKeyBytes(), backingDomain, "[email protected]", "[email protected]");
String responseJson = signedResponse.toJson();
PayReqResponse parsedResponse = umaProtocolHelper.parseAsPayReqResponse(responseJson);
assertNotNull(parsedResponse);
JsonObject payeeData = parsedResponse.payeeData();
assertNotNull(payeeData);
CompliancePayeeData complianceData = getSerialFormat().decodeFromJsonElement(
CompliancePayeeData.Companion.serializer(),
payeeData.get("compliance")
);
assertNotNull(complianceData);
assertNotNull(complianceData.getBackingSignatures());
assertEquals(1, complianceData.getBackingSignatures().size());
Long publicKeyCacheExpiry = System.currentTimeMillis() / 1000 + 10000;
PubKeyResponse backingVaspPubKeyResponse = new PubKeyResponse(publicKeyBytes(), publicKeyBytes(), publicKeyCacheExpiry);
publicKeyCache.addPublicKeysForVasp(backingDomain, backingVaspPubKeyResponse);
assertTrue(umaProtocolHelper.verifyPayReqResponseBackingSignaturesSync(parsedResponse, "[email protected]"));
}

@Test
public void testGetPayReqResponseFuture() throws Exception {
PayRequest request = umaProtocolHelper.getPayRequest(
Expand Down Expand Up @@ -440,6 +580,32 @@ public void testVerifyUmaLnurlpQuerySignature_purgeOlderNoncesAndStoreNonce() th
requireNonNull(request.asUmaRequest()), new PubKeyResponse(publicKeyBytes(), publicKeyBytes()), nonceCache));
}

@Test
public void testSignAndVerifyLnurlpRequestWithBackingSignature() throws Exception {
String lnurlpUrl = umaProtocolHelper.getSignedLnurlpRequestUrl(
privateKeyBytes(),
"[email protected]",
"https://vasp.com",
true);
LnurlpRequest request = umaProtocolHelper.parseLnurlpRequest(lnurlpUrl);
assertNotNull(request);
byte[] backingVaspPrivateKey = privateKeyBytes();
String backingDomain = "backingvasp.com";
UmaLnurlpRequest requestWithBackingSignature = request.asUmaRequest().appendBackingSignature(backingVaspPrivateKey, backingDomain);
String encodedUrl = requestWithBackingSignature.encodeToUrl();
LnurlpRequest parsedRequest = umaProtocolHelper.parseLnurlpRequest(encodedUrl);
assertNotNull(parsedRequest);
assertNotNull(parsedRequest.getBackingSignatures());
assertEquals(1, parsedRequest.getBackingSignatures().size());
Long publicKeyCacheExpiry = System.currentTimeMillis() / 1000 + 10000;
PubKeyResponse backingVaspPubKeyResponse = new PubKeyResponse(publicKeyBytes(), publicKeyBytes(), publicKeyCacheExpiry);
publicKeyCache.addPublicKeysForVasp(backingDomain, backingVaspPubKeyResponse);
assertTrue(umaProtocolHelper.verifyUmaLnurlpQuerySignature(requireNonNull(request.asUmaRequest()), new PubKeyResponse(publicKeyBytes(), publicKeyBytes()), new InMemoryNonceCache(1L)));
assertTrue(umaProtocolHelper.verifyUmaLnurlpQueryBackingSignaturesSync(
requireNonNull(parsedRequest.asUmaRequest())
));
}

@Test
public void testGetAndVerifyPostTransactionCallback() throws Exception {
PostTransactionCallback callback = umaProtocolHelper.getPostTransactionCallback(
Expand Down
159 changes: 159 additions & 0 deletions uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,43 @@ class UmaProtocolHelper @JvmOverloads constructor(
)
}

/**
* Verifies the backing signatures on an UMA Lnurlp query. You may optionally call this function after
* verifyUmaLnurlpQuerySignature to verify signatures from backing VASPs.
*
* @param query The signed query to verify.
* @return true if all backing signatures are valid, false otherwise.
*/
@Throws(Exception::class)
fun verifyUmaLnurlpQueryBackingSignaturesSync(query: UmaLnurlpRequest): Boolean = runBlocking {
verifyUmaLnurlpQueryBackingSignatures(query)
}

/**
* Verifies the backing signatures on an UMA Lnurlp query. You may optionally call this function after
* verifyUmaLnurlpQuerySignature to verify signatures from backing VASPs.
*
* @param query The signed query to verify.
* @return true if all backing signatures are valid, false otherwise.
*/
@JvmName("KotlinOnly-verifyUmaLnurlpQueryBackingSignaturesSuspended")
@Throws(Exception::class)
suspend fun verifyUmaLnurlpQueryBackingSignatures(query: UmaLnurlpRequest): Boolean {
val backingSignatures = query.backingSignatures ?: return true
for (backingSignature in backingSignatures) {
val backingVaspPubKeyResponse = fetchPublicKeysForVasp(backingSignature.domain)
val isSignatureValid = verifySignature(
query.signablePayload(),
backingSignature.signature,
backingVaspPubKeyResponse.getSigningPublicKey()
)
if (!isSignatureValid) {
return false
}
}
return true
}

/**
* Creates a signed UMA [LnurlpResponse].
*
Expand Down Expand Up @@ -305,6 +342,43 @@ class UmaProtocolHelper @JvmOverloads constructor(
)
}

/**
* Verifies the backing signatures on an UMA Lnurlp response. You may optionally call this function after
* verifyLnurlpResponseSignature to verify signatures from backing VASPs.
*
* @param response The signed [LnurlpResponse] to verify.
* @return true if all backing signatures are valid, false otherwise.
*/
@Throws(Exception::class)
fun verifyLnurlpResponseBackingSignaturesSync(response: UmaLnurlpResponse): Boolean = runBlocking {
verifyLnurlpResponseBackingSignatures(response)
}

/**
* Verifies the backing signatures on an UMA Lnurlp response. You may optionally call this function after
* verifyLnurlpResponseSignature to verify signatures from backing VASPs.
*
* @param response The signed [LnurlpResponse] to verify.
* @return true if all backing signatures are valid, false otherwise.
*/
@JvmName("KotlinOnly-verifyLnurlpResponseBackingSignaturesSuspended")
@Throws(Exception::class)
suspend fun verifyLnurlpResponseBackingSignatures(response: UmaLnurlpResponse): Boolean {
val backingSignatures = response.backingSignatures ?: return true
for (backingSignature in backingSignatures) {
val backingVaspPubKeyResponse = fetchPublicKeysForVasp(backingSignature.domain)
val isSignatureValid = verifySignature(
response.compliance.signablePayload(),
backingSignature.signature,
backingVaspPubKeyResponse.getSigningPublicKey()
)
if (!isSignatureValid) {
return false
}
}
return true
}

/**
* Creates a signed UMA [PayRequest].
*
Expand Down Expand Up @@ -468,6 +542,45 @@ class UmaProtocolHelper @JvmOverloads constructor(
)
}

/**
* Verifies the backing signatures on a [PayRequest]. You may optionally call this function after
* verifyPayReqBackingSignatures to verify signatures from backing VASPs.
*
* @param payReq The signed [PayRequest] to verify.
* @return true if all backing signatures are valid, false otherwise.
*/
@Throws(Exception::class)
fun verifyPayReqBackingSignaturesSync(payReq: PayRequest): Boolean = runBlocking {
verifyPayReqBackingSignatures(payReq)
}

/**
* Verifies the backing signatures on a [PayRequest]. You may optionally call this function after
* verifyPayReqBackingSignatures to verify signatures from backing VASPs.
*
* @param payReq The signed [PayRequest] to verify.
* @return true if all backing signatures are valid, false otherwise.
*/
@JvmName("KotlinOnly-verifyPayReqBackingSignaturesSuspended")
@Throws(Exception::class)
suspend fun verifyPayReqBackingSignatures(payReq: PayRequest): Boolean {
if (!payReq.isUmaRequest()) return false
val compliance = payReq.payerData?.compliance() ?: return false
val backingSignatures = compliance.backingSignatures ?: return true
for (backingSignature in backingSignatures) {
val backingVaspPubKeyResponse = fetchPublicKeysForVasp(backingSignature.domain)
val isSignatureValid = verifySignature(
payReq.signablePayload(),
backingSignature.signature,
backingVaspPubKeyResponse.getSigningPublicKey()
)
if (!isSignatureValid) {
return false
}
}
return true
}

/**
* Creates an uma pay request response with an encoded invoice.
*
Expand Down Expand Up @@ -830,6 +943,52 @@ class UmaProtocolHelper @JvmOverloads constructor(
)
}

/**
* Verifies the backing signatures on a [PayReqResponse]. You may optionally call this function after
* verifyPayReqResponseSignature to verify signatures from backing VASPs.
*
* @param payReqResponse The signed [PayReqResponse] to verify.
* @param payerIdentifier The identifier of the sender. For example, [email protected]
* @return true if all backing signatures are valid, false otherwise.
*/
@Throws(Exception::class)
fun verifyPayReqResponseBackingSignaturesSync(payReqResponse: PayReqResponse, payerIdentifier: String): Boolean =
runBlocking {
verifyPayReqResponseBackingSignatures(payReqResponse, payerIdentifier)
}

/**
* Verifies the backing signatures on a [PayReqResponse]. You may optionally call this function after
* verifyPayReqResponseSignature to verify signatures from backing VASPs.
*
* @param payReqResponse The signed [PayReqResponse] to verify.
* @param payerIdentifier The identifier of the sender. For example, [email protected]
* @return true if all backing signatures are valid, false otherwise.
*/
@JvmName("KotlinOnly-verifyPayReqResponseBackingSignaturesSuspended")
@Throws(Exception::class)
suspend fun verifyPayReqResponseBackingSignatures(
payReqResponse: PayReqResponse,
payerIdentifier: String,
): Boolean {
if (payReqResponse !is PayReqResponseV1) return true
if (!payReqResponse.isUmaResponse()) return false
val compliance = payReqResponse.payeeData?.payeeCompliance() ?: return false
val backingSignatures = compliance.backingSignatures ?: return true
for (backingSignature in backingSignatures) {
val backingVaspPubKeyResponse = fetchPublicKeysForVasp(backingSignature.domain)
val isSignatureValid = verifySignature(
payReqResponse.signablePayload(payerIdentifier),
backingSignature.signature,
backingVaspPubKeyResponse.getSigningPublicKey()
)
if (!isSignatureValid) {
return false
}
}
return true
}

/**
* Creates a signed [PostTransactionCallback].
*
Expand Down
Loading
Loading