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

feat: update challenge transaction helpers for SEP-0010 v3.0.0. #308

Merged
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
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) {
overcat marked this conversation as resolved.
Show resolved Hide resolved
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