Skip to content

Commit

Permalink
feat: update challenge transaction helpers for SEP-0010 v3.0.0. (#308)
Browse files Browse the repository at this point in the history
Co-authored-by: Jake Urban <[email protected]>
Co-authored-by: Leigh McCulloch <[email protected]>
  • Loading branch information
3 people authored Dec 10, 2020
1 parent 7b3d60e commit 471715f
Show file tree
Hide file tree
Showing 3 changed files with 320 additions and 100 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
As this project is pre 1.0, breaking changes may happen for minor version bumps. A breaking change will get clearly notified in this log.

## 0.21.2
- Update challenge transaction helpers for SEP-0010 v3.0.0. ([#308](https://github.com/stellar/java-stellar-sdk/pull/308))
- Fix the decoding of `balanceId` in `org.stellar.sdk.ClaimClaimableBalanceOperation`. ([#310](https://github.com/stellar/java-stellar-sdk/pull/310))

## 0.21.1
Expand Down
123 changes: 110 additions & 13 deletions src/main/java/org/stellar/sdk/Sep10Challenge.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class Sep10Challenge {
* @param signer The server's signing account.
* @param network The Stellar network used by the server.
* @param clientAccountId The stellar account belonging to the client.
* @param domainName The <a href="https://en.wikipedia.org/wiki/Fully_qualified_domain_name" target="_blank">fully qualified domain name</a> of the service requiring authentication (The domainName field is reserved for future use and not used).
* @param domainName The <a href="https://en.wikipedia.org/wiki/Fully_qualified_domain_name" target="_blank">fully qualified domain name</a> of the service requiring authentication.
* @param timebounds The lifetime of the challenge token.
*/
public static Transaction newChallenge(
Expand Down Expand Up @@ -64,17 +64,23 @@ public static Transaction newChallenge(
* that any signatures other than the servers on the transaction are valid. Use
* one of the following functions to completely verify the transaction:
* {@link Sep10Challenge#verifyChallengeTransactionSigners(String, String, Network, String, Set)} or
* {@link Sep10Challenge#verifyChallengeTransactionThreshold(String, String, Network, String, int, Set)}
* {@link Sep10Challenge#verifyChallengeTransactionThreshold(String, String, Network, String, int, Set)} or
* {@link Sep10Challenge#verifyChallengeTransactionSigners(String, String, Network, String[], Set)} or
* {@link Sep10Challenge#verifyChallengeTransactionThreshold(String, String, Network, String[], int, Set)} or
*
* @param challengeXdr SEP-0010 transaction challenge transaction in base64.
* @param serverAccountId Account ID for server's account.
* @param network The network to connect to for verifying and retrieving.
* @param domainName The <a href="https://en.wikipedia.org/wiki/Fully_qualified_domain_name" target="_blank">fully qualified domain name</a> of the service requiring authentication (The domainName field is reserved for future use and not used).
* @param domainNames An array of home domains, one of which is expected to be included in the first Manage Data operation's string key.
* @return {@link ChallengeTransaction}, the decoded transaction envelope and client account ID contained within.
* @throws InvalidSep10ChallengeException If the SEP-0010 validation fails, the exception will be thrown.
* @throws IOException If read XDR string fails, the exception will be thrown.
*/
public static ChallengeTransaction readChallengeTransaction(String challengeXdr, String serverAccountId, Network network, String domainName) throws InvalidSep10ChallengeException, IOException {
public static ChallengeTransaction readChallengeTransaction(String challengeXdr, String serverAccountId, Network network, String[] domainNames) throws InvalidSep10ChallengeException, IOException {
if (domainNames == null || domainNames.length == 0) {
throw new IllegalArgumentException("At least one domain name must be included in domainNames.");
}

// decode the received input as a base64-urlencoded XDR representation of Stellar transaction envelope
AbstractTransaction parsed = Transaction.fromEnvelopeXdr(challengeXdr, network);
if (!(parsed instanceof Transaction)) {
Expand Down Expand Up @@ -130,10 +136,25 @@ public static ChallengeTransaction readChallengeTransaction(String challengeXdr,
throw new InvalidSep10ChallengeException("Operation should have a source account.");
}

String matchedDomainName = null;
for (String homeDomain : domainNames) {
if ((homeDomain + " " + MANAGER_DATA_NAME_FLAG).equals(manageDataOperation.getName())) {
matchedDomainName = homeDomain;
break;
}
}

if (matchedDomainName == null) {
throw new InvalidSep10ChallengeException("The transaction's operation key name does not include one of the expected home domains.");
}

if (StrKey.decodeVersionByte(clientAccountId) != StrKey.VersionByte.ACCOUNT_ID) {
throw new InvalidSep10ChallengeException("clientAccountId: "+clientAccountId+" is not a valid account id");
}

if (manageDataOperation.getValue() == null) {
throw new InvalidSep10ChallengeException("The transaction's operation value should not be null.");
}
// verify manage data value
if (manageDataOperation.getValue().length != 64) {
throw new InvalidSep10ChallengeException("Random nonce encoded as base64 should be 64 bytes long.");
Expand Down Expand Up @@ -170,7 +191,32 @@ public static ChallengeTransaction readChallengeTransaction(String challengeXdr,
throw new InvalidSep10ChallengeException(String.format("Transaction not signed by server: %s.", serverAccountId));
}

return new ChallengeTransaction(transaction, clientAccountId);
return new ChallengeTransaction(transaction, clientAccountId, matchedDomainName);
}

/**
* Reads a SEP 10 challenge transaction and returns the decoded transaction envelope and client account ID contained within.
* <p>
* It also verifies that transaction is signed by the server.
* <p>
* It does not verify that the transaction has been signed by the client or
* that any signatures other than the servers on the transaction are valid. Use
* one of the following functions to completely verify the transaction:
* {@link Sep10Challenge#verifyChallengeTransactionSigners(String, String, Network, String, Set)} or
* {@link Sep10Challenge#verifyChallengeTransactionThreshold(String, String, Network, String, int, Set)} or
* {@link Sep10Challenge#verifyChallengeTransactionSigners(String, String, Network, String[], Set)} or
* {@link Sep10Challenge#verifyChallengeTransactionThreshold(String, String, Network, String[], int, Set)} or
*
* @param challengeXdr SEP-0010 transaction challenge transaction in base64.
* @param serverAccountId Account ID for server's account.
* @param network The network to connect to for verifying and retrieving.
* @param domainName The home domain that is expected to be included in the first Manage Data operation's string key.
* @return {@link ChallengeTransaction}, the decoded transaction envelope and client account ID contained within.
* @throws InvalidSep10ChallengeException If the SEP-0010 validation fails, the exception will be thrown.
* @throws IOException If read XDR string fails, the exception will be thrown.
*/
public static ChallengeTransaction readChallengeTransaction(String challengeXdr, String serverAccountId, Network network, String domainName) throws InvalidSep10ChallengeException, IOException {
return readChallengeTransaction(challengeXdr, serverAccountId, network, new String[]{domainName});
}

/**
Expand All @@ -185,19 +231,41 @@ public static ChallengeTransaction readChallengeTransaction(String challengeXdr,
* @param challengeXdr SEP-0010 transaction challenge transaction in base64.
* @param serverAccountId Account ID for server's account.
* @param network The network to connect to for verifying and retrieving.
* @param domainName The <a href="https://en.wikipedia.org/wiki/Fully_qualified_domain_name" target="_blank">fully qualified domain name</a> of the service requiring authentication (The domainName field is reserved for future use and not used).
* @param domainName The home domain that is expected to be included in the first Manage Data operation's string key.
* @param signers The signers of client account.
* @return a list of signers that were found is returned, excluding the server account ID.
* @throws InvalidSep10ChallengeException If the SEP-0010 validation fails, the exception will be thrown.
* @throws IOException If read XDR string fails, the exception will be thrown.
*/
public static Set<String> verifyChallengeTransactionSigners(String challengeXdr, String serverAccountId, Network network, String domainName, Set<String> signers) throws InvalidSep10ChallengeException, IOException {
return verifyChallengeTransactionSigners(challengeXdr, serverAccountId, network, new String[]{domainName}, signers);
}

/**
* Verifies that for a SEP 10 challenge transaction
* all signatures on the transaction are accounted for. A transaction is
* verified if it is signed by the server account, and all other signatures
* match a signer that has been provided as an argument. Additional signers can
* be provided that do not have a signature, but all signatures must be matched
* to a signer for verification to succeed. If verification succeeds a list of
* signers that were found is returned, excluding the server account ID.
*
* @param challengeXdr SEP-0010 transaction challenge transaction in base64.
* @param serverAccountId Account ID for server's account.
* @param network The network to connect to for verifying and retrieving.
* @param domainNames An array of home domains, one of which is expected to be included in the first Manage Data operation's string key.
* @param signers The signers of client account.
* @return a list of signers that were found is returned, excluding the server account ID.
* @throws InvalidSep10ChallengeException If the SEP-0010 validation fails, the exception will be thrown.
* @throws IOException If read XDR string fails, the exception will be thrown.
*/
public static Set<String> verifyChallengeTransactionSigners(String challengeXdr, String serverAccountId, Network network, String[] domainNames, Set<String> signers) throws InvalidSep10ChallengeException, IOException {
if (signers == null || signers.isEmpty()) {
throw new InvalidSep10ChallengeException("No verifiable signers provided, at least one G... address must be provided.");
}

// Read the transaction which validates its structure.
ChallengeTransaction parsedChallengeTransaction = readChallengeTransaction(challengeXdr, serverAccountId, network, domainName);
ChallengeTransaction parsedChallengeTransaction = readChallengeTransaction(challengeXdr, serverAccountId, network, domainNames);
Transaction transaction = parsedChallengeTransaction.getTransaction();

// Ensure the server account ID is an address and not a seed.
Expand Down Expand Up @@ -275,14 +343,14 @@ public static Set<String> verifyChallengeTransactionSigners(String challengeXdr,
* @param challengeXdr SEP-0010 transaction challenge transaction in base64.
* @param serverAccountId Account ID for server's account.
* @param network The network to connect to for verifying and retrieving.
* @param domainName The <a href="https://en.wikipedia.org/wiki/Fully_qualified_domain_name" target="_blank">fully qualified domain name</a> of the service requiring authentication (The domainName field is reserved for future use and not used).
* @param domainNames An array of home domains, one of which is expected to be included in the first Manage Data operation's string key.
* @param threshold The threshold on the client account.
* @param signers The signers of client account.
* @return a list of signers that were found is returned, excluding the server account ID.
* @throws InvalidSep10ChallengeException If the SEP-0010 validation fails, the exception will be thrown.
* @throws IOException If read XDR string fails, the exception will be thrown.
*/
public static Set<String> verifyChallengeTransactionThreshold(String challengeXdr, String serverAccountId, Network network, String domainName, int threshold, Set<Signer> signers) throws InvalidSep10ChallengeException, IOException {
public static Set<String> verifyChallengeTransactionThreshold(String challengeXdr, String serverAccountId, Network network, String[] domainNames, int threshold, Set<Signer> signers) throws InvalidSep10ChallengeException, IOException {
if (signers == null || signers.isEmpty()) {
throw new InvalidSep10ChallengeException("No verifiable signers provided, at least one G... address must be provided.");
}
Expand All @@ -292,7 +360,7 @@ public static Set<String> verifyChallengeTransactionThreshold(String challengeXd
weightsForSigner.put(signer.getKey(), signer.getWeight());
}

Set<String> signersFound = verifyChallengeTransactionSigners(challengeXdr, serverAccountId, network, domainName, weightsForSigner.keySet());
Set<String> signersFound = verifyChallengeTransactionSigners(challengeXdr, serverAccountId, network, domainNames, weightsForSigner.keySet());

int sum = 0;
for (String signer : signersFound) {
Expand All @@ -309,6 +377,28 @@ public static Set<String> verifyChallengeTransactionThreshold(String challengeXd
return signersFound;
}

/**
* Verifies that for a SEP-0010 challenge transaction
* all signatures on the transaction are accounted for and that the signatures
* meet a threshold on an account. A transaction is verified if it is signed by
* the server account, and all other signatures match a signer that has been
* provided as an argument, and those signatures meet a threshold on the
* account.
*
* @param challengeXdr SEP-0010 transaction challenge transaction in base64.
* @param serverAccountId Account ID for server's account.
* @param network The network to connect to for verifying and retrieving.
* @param domainName The home domain that is expected to be included in the first Manage Data operation's string key.
* @param threshold The threshold on the client account.
* @param signers The signers of client account.
* @return a list of signers that were found is returned, excluding the server account ID.
* @throws InvalidSep10ChallengeException If the SEP-0010 validation fails, the exception will be thrown.
* @throws IOException If read XDR string fails, the exception will be thrown.
*/
public static Set<String> verifyChallengeTransactionThreshold(String challengeXdr, String serverAccountId, Network network, String domainName, int threshold, Set<Signer> signers) throws InvalidSep10ChallengeException, IOException {
return verifyChallengeTransactionThreshold(challengeXdr, serverAccountId, network, new String[]{domainName}, threshold, signers);
}

private static Set<String> verifyTransactionSignatures(Transaction transaction, Set<String> signers) throws InvalidSep10ChallengeException {
if (transaction.getSignatures().isEmpty()) {
throw new InvalidSep10ChallengeException("Transaction has no signatures.");
Expand Down Expand Up @@ -346,15 +436,17 @@ private static boolean verifyTransactionSignature(Transaction transaction, Strin
}

/**
* Used to store the results produced by {@link Sep10Challenge#readChallengeTransaction(String, String, Network, String)}.
* Used to store the results produced by {@link Sep10Challenge#readChallengeTransaction(String, String, Network, String[])}.
*/
public static class ChallengeTransaction {
private final Transaction transaction;
private final String clientAccountId;
private final String matchedHomeDomain;

public ChallengeTransaction(Transaction transaction, String clientAccountId) {
public ChallengeTransaction(Transaction transaction, String clientAccountId, String matchedHomeDomain) {
this.transaction = transaction;
this.clientAccountId = clientAccountId;
this.matchedHomeDomain = matchedHomeDomain;
}

public Transaction getTransaction() {
Expand All @@ -365,6 +457,10 @@ public String getClientAccountId() {
return clientAccountId;
}

public String getMatchedHomeDomain() {
return matchedHomeDomain;
}

@Override
public int hashCode() {
return Objects.hashCode(this.transaction.hashHex(), this.clientAccountId);
Expand All @@ -382,7 +478,8 @@ public boolean equals(Object object) {

ChallengeTransaction other = (ChallengeTransaction) object;
return Objects.equal(this.transaction.hashHex(), other.transaction.hashHex()) &&
Objects.equal(this.clientAccountId, other.clientAccountId);
Objects.equal(this.clientAccountId, other.clientAccountId) &&
Objects.equal(this.matchedHomeDomain, other.matchedHomeDomain);
}
}

Expand Down
Loading

0 comments on commit 471715f

Please sign in to comment.