diff --git a/.gitignore b/.gitignore index f8db48c..779c82c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -reports \ No newline at end of file +reports +junit.xml \ No newline at end of file diff --git a/cases/sep10.test.js b/cases/sep10.test.js index f2767d4..4c8f8c7 100644 --- a/cases/sep10.test.js +++ b/cases/sep10.test.js @@ -2,11 +2,17 @@ import { fetch } from "./util/fetchShim"; import JWT from "jsonwebtoken"; import TOML from "toml"; import StellarSDK from "stellar-sdk"; +import friendbot from "./util/friendbot"; +import getSep10Token from "./util/sep10"; +// Friendbot usage makes this very slow. Need to create an account pool at +// the beginning and reuse them. +jest.setTimeout(100000); const url = process.env.DOMAIN; const account = "GCQJX6WGG7SSFU2RBO5QANTFXY7C5GTTFJDCBAAO42JCCFIMZ7PEBURP"; const secret = "SAUOSXXF7ZDO5PKHRFR445DRKZ66Q5HIM2HIPQGWBTUKJZQAOP3VGH3L"; const keyPair = StellarSDK.Keypair.fromSecret(secret); +const server = new StellarSDK.Server("https://horizon-testnet.stellar.org"); describe("SEP10", () => { let toml; @@ -187,4 +193,146 @@ describe("SEP10", () => { }); }); }); + + describe("signers support", () => { + afterAll(async () => { + console.log("DESTROY ALL FRIENDS"); + await friendbot.destroyAllFriends(); + }); + + /** + * Removing the masterWeight for an account means that it can + * no longer sign for itself. This should mean that it can't + * get a token with its own signature. + */ + it("fails for an account that can't sign for itself", async () => { + const accountA = StellarSDK.Keypair.random(); + await friendbot(accountA); + const accountData = await server.loadAccount(accountA.publicKey()); + const transaction = new StellarSDK.TransactionBuilder(accountData, { + fee: StellarSDK.BASE_FEE, + networkPassphrase: StellarSDK.Networks.TESTNET + }) + .addOperation( + StellarSDK.Operation.setOptions({ + masterWeight: 0 + }) + ) + .setTimeout(30) + .build(); + transaction.sign(accountA); + await server.submitTransaction(transaction); + const token = await getSep10Token(url, accountA, [accountA]); + expect(token).toBeFalsy(); + }); + + it("succeeds for a signer of an account", async () => { + const userAccount = StellarSDK.Keypair.random(); + const signerAccount = StellarSDK.Keypair.random(); + await Promise.all([friendbot(userAccount), friendbot(signerAccount)]); + const accountData = await server.loadAccount(userAccount.publicKey()); + const transaction = new StellarSDK.TransactionBuilder(accountData, { + fee: StellarSDK.BASE_FEE, + networkPassphrase: StellarSDK.Networks.TESTNET + }) + .addOperation( + StellarSDK.Operation.setOptions({ + lowThreshold: 1, + medThreshold: 1, + highThreshold: 1, + signer: { + ed25519PublicKey: signerAccount.publicKey(), + weight: 1 + } + }) + ) + .setTimeout(30) + .build(); + transaction.sign(userAccount); + await server.submitTransaction(transaction); + const token = await getSep10Token(url, userAccount, [signerAccount]); + expect(token).toBeTruthy(); + }); + + /** + * In this test case, since we have a signer with only half the required + * weight of the thresholds, a malicious actor might try to sign twice + * with the same key hoping the server doesn't de-duplicate signers, and + * count its weight twice. + */ + it("fails when trying to reuse the same signer to gain weight", async () => { + const userAccount = StellarSDK.Keypair.random(); + const signerAccount = StellarSDK.Keypair.random(); + await Promise.all([friendbot(userAccount), friendbot(signerAccount)]); + const accountData = await server.loadAccount(userAccount.publicKey()); + const transaction = new StellarSDK.TransactionBuilder(accountData, { + fee: StellarSDK.BASE_FEE, + networkPassphrase: StellarSDK.Networks.TESTNET + }) + .addOperation( + StellarSDK.Operation.setOptions({ + lowThreshold: 2, + medThreshold: 2, + highThreshold: 2, + signer: { + ed25519PublicKey: signerAccount.publicKey(), + weight: 1 + } + }) + ) + .setTimeout(30) + .build(); + transaction.sign(userAccount); + await server.submitTransaction(transaction); + const token = await getSep10Token(url, userAccount, [ + signerAccount, + signerAccount + ]); + expect(token).toBeFalsy(); + }); + + it("succeeds with multiple signers", async () => { + const userAccount = StellarSDK.Keypair.random(); + const signerAccount1 = StellarSDK.Keypair.random(); + const signerAccount2 = StellarSDK.Keypair.random(); + await Promise.all([ + friendbot(userAccount), + friendbot(signerAccount1), + friendbot(signerAccount2) + ]); + const accountData = await server.loadAccount(userAccount.publicKey()); + const transaction = new StellarSDK.TransactionBuilder(accountData, { + fee: StellarSDK.BASE_FEE, + networkPassphrase: StellarSDK.Networks.TESTNET + }) + .addOperation( + StellarSDK.Operation.setOptions({ + lowThreshold: 2, + medThreshold: 2, + highThreshold: 2, + signer: { + ed25519PublicKey: signerAccount1.publicKey(), + weight: 1 + } + }) + ) + .addOperation( + StellarSDK.Operation.setOptions({ + signer: { + ed25519PublicKey: signerAccount2.publicKey(), + weight: 1 + } + }) + ) + .setTimeout(30) + .build(); + transaction.sign(userAccount); + await server.submitTransaction(transaction); + const token = await getSep10Token(url, userAccount, [ + signerAccount1, + signerAccount2 + ]); + expect(token).toBeTruthy(); + }); + }); }); diff --git a/cases/util/friendbot.js b/cases/util/friendbot.js new file mode 100644 index 0000000..dc186f8 --- /dev/null +++ b/cases/util/friendbot.js @@ -0,0 +1,36 @@ +import { fetch } from "./fetchShim"; +import StellarSdk from "stellar-sdk"; + +const friends = []; +async function friendbot(keyPair) { + const response = await fetch( + `https://friendbot.stellar.org/?addr=${keyPair.publicKey()}` + ); + friends.push(keyPair); + expect(response.status).toEqual(200); +} + +friendbot.destroyAllFriends = async function() { + if (friends.length === 0) return; + const accountData = await server.loadAccount(friends[0].publicKey()); + const transactionBuilder = new StellarSDK.TransactionBuilder(accountData, { + fee: StellarSDK.BASE_FEE, + networkPassphrase: StellarSDK.Networks.TESTNET + }); + friends.forEach(keyPair => { + transactionBuilder.addOperation( + StellarSdk.Operations.accountMerge({ + destination: "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", + source: keyPair.publicKey() + }) + ); + }); + + const tx = transactionBuilder.setTimeout(30).build(); + friends.forEach(keyPair => { + tx.sign(keyPair); + }); + await server.submitTransaction(tx); +}; + +export default friendbot; diff --git a/cases/util/sep10.js b/cases/util/sep10.js index 071f6c8..f35b864 100644 --- a/cases/util/sep10.js +++ b/cases/util/sep10.js @@ -2,7 +2,8 @@ import TOML from "toml"; import StellarSDK from "stellar-sdk"; import { fetch } from "./fetchShim"; -export default async function getSep10Token(domain, keyPair) { +export default async function getSep10Token(domain, keyPair, signers) { + if (!signers) signers = [keyPair]; let response = await fetch(domain + "/.well-known/stellar.toml"); const text = await response.text(); const toml = TOML.parse(text); @@ -14,7 +15,9 @@ export default async function getSep10Token(domain, keyPair) { json.transaction, json.network_passphrase ); - tx.sign(keyPair); + signers.forEach(keyPair => { + tx.sign(keyPair); + }); let resp = await fetch(toml.WEB_AUTH_ENDPOINT, { method: "POST", headers: {