-
Notifications
You must be signed in to change notification settings - Fork 1
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
Add client domain corresponding signature when needed. #236
Conversation
✅ Deploy Preview for pendulum-pay ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
|
||
const transactionSigned = new TransactionBuilder.fromXDR(challengeXDR, NETWORK_PASSPHRASE); | ||
if (transactionSigned.source !== signingKey) { | ||
throw new Error(`Invalid source account: ${transactionSigned.source}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are fetching the signingKey
of the anchor from our own .toml
url. I see no vector from which someone could send a malicious transaction for us to sign.
This, in conjunction with the 0
sequence number check.
@pendulum-chain/devs added to the security concern, we of course have this endpoint open, so although we will not sign any transaction that would withdraw funds, any person could request this signature and act in our regard as Client Application. We discussed at some point for the This case will be harder to protect, since this operation is performed at the beginning and there is no guarantee that the user will go through with the off-ramp with us. If we then reverse the order of operations and make the user first sign the Therefore the best option I see is to modify the code such that the UI first signs the |
Following on the above, I think the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's think about the attack scenario and solutions here. But I don't think that we need to implement them yet (more below).
So what is the attack vector. I think it is just that someone could impersonate us by letting their own sep-10 challenge be signed by us. There is no way to prevent that with this endpoint as the endpoint can't know who created the challenge.
To make this secure, I think that the endpoint itself needs to request the sep-10 challenge. So the frontend does not request it directly but asks the service to request it. The backend can trust the anchor and the frontend can trust the backend.
However, this is still not safe as the backend can't trust the frontend. So I think that in order to make it secure, the backend should never return the resulting sep-10 token to the frontend. This means that the backend needs to initiate sep-24 as well. Would this then prevent the attack scenario? I think so.
Anyway, this could be too much effort for now. I think we should still keep it simple. Let's rather maintain a list of known weaknesses and solutions and prioritize them as required.
@@ -0,0 +1,27 @@ | |||
const fetchTomlValues = async (TOML_FILE_URL) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since TOML_FILE_URL
here is a function argument and not a global constant, I would rather use normal camelCase.
@@ -8,4 +8,6 @@ router.route('/create').post(validateCreationInput, controller.createStellarTran | |||
|
|||
router.route('/payment').post(validateChangeOpInput, controller.changeOpTransaction); | |||
|
|||
router.route('/sep10').post(controller.signSep10Challenge); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shall we also add a validation function that checks for a proper challengeXDR
and outToken
in the request body?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I will add checks now regarding the proper operations that the challenge needs to have. This way we can then fully trust the fronted in that it is not sending any incorrect challenge.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I misunderstood this comment, you mean the validator only to check if it's not empty right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, just the simple validator like the other endpoints.
src/services/anchor/index.ts
Outdated
if (requiresClientDomain) { | ||
urlParams = new URLSearchParams({ | ||
account: accountId, | ||
client_domain: 'https://satoshipay.io/.well-known/stellar.toml', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be good to put this url to a configuration file instead of here in the code.
src/services/signingService.tsx
Outdated
challengeXDR: string, | ||
outToken: OutputTokenType, | ||
): Promise<ClientDomainSep10Response> => { | ||
const response = await fetch(`http://localhost:3000/v1/stellar/sep10`, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This domain needs to be changed of course.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's for ease of testing.
@TorstenStueber I thought the same, but then I proposed this hypothesis. If we add these extra checks I think we can trust the frontend, at least in terms of security. The first On the other hand, we could initiate the But there is the impersonating issue. Even with all those checks in place, a "malicious" application could request the authentication with this endpoint, and later just send the funds directly to the anchor's deposit account while benefiting from our special fees, because there are no funds commitment. Is there even a way to prevent this fundamentally? |
throw new Error('The first operation should be manageData'); | ||
} | ||
// We don't want to accept a challenge that would authorize as Application client account! | ||
if (firstOp.source !== clientPublicKey || firstOp.source == signingKey) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this is the key check for security regarding this topic and subsequent comments.
We return the signature that should only be valid to authenticate the user public key.
745dbbc
to
6cdf65a
Compare
In relation with this issue which I think is very connected (it may also need the In that case we would need to add a check that the I also attempted a |
The problem in my opinion is not that the backend receives an incorrect challenge – of course it is still reasonable to make all required test to check the validity of the challenge (I did not do when I wrote the sep-10 code initially). The real problem is impersonating SatoshiPay/Vortex. Any malicious app could send their own SEP-10 challenge to use for cosigning. How can we detect that? I think only if we initiate SEP-10 from the backend itself. You are right, we don't need to execute SEP-24 on the backend, the resulting JWT would already contain only our own account (if we don't use the ephemeral account for SEP-10 anymore). |
When you say that an "app could send their own SEP-10 challenge" what do you mean? Because we will detect before signing all the signatures and memos. This is very connected also with the polygon verification and we should solve both at the same time, I think, at least the ideal solution for the future. It doesn't matter who starts the The only way I see to solve this (besides being in control of the funds) is to implement the contract we discussed way at the beginning of the prototype. User would first transfer the tokens to that contract, they will be paused until the backend gives the "green light" to proceed. We can ensure that the polygon user is in control of the account given this first transfer happened in the first place(so no challenge), and use the account itself for memo. When the user finishes the KYC, we allow the contract to proceed with the swap, etc. |
Another app could request a SEP-10 challenge from the anchor, providing our client_domain. Then they could just call our signing-service endpoint to add the I don't think that we need a contract to solve it if our backend initiates the SEP-10 itself. Right now of course the backend does not control the rest of the flow and it would only start to be secure when the backend executes all the remaining flow, but we will implement it eventually that way. |
Yes totally, I think we are talking about the same potential issue, I also agree that is enough for now, I wanted to be sure that there exists a secure modification that we can do with some extra effort, even if we don't do it now. So how should we proceed with this now? Once the anchor accepts our |
I am fine with using the implementation you did here. Instead of optimizing for the ideal solution now, let's rather move forward and improve this later. |
@pendulum-chain/devs I merged the changes from #201, the |
} | ||
|
||
// Only authorize a session that corresponds with the ephemeral client account | ||
if (firstOp.source !== clientPublicKey) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
clientPublicKey
is supplied by the user, so this doesn't add protection against the UI. It is mostly an extra check to ensure the challenge operation is consistent.
throw new Error('Transaction must contain a client_domain manageData operation'); | ||
} | ||
|
||
let clientDomainSignature; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is our barrier against signing for Anchors that we don't to sign client domain. Since above we also fetch the anchorSigningKey
corresponding to the token type, the UI shouldn't be able to game this.
if (!hasClientDomain && clientDomainEnabled) { | ||
throw new Error('Transaction must contain a client_domain manageData operation'); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we alleviate this restriction as it's marked as optional in the protocol?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's mostly for us, and check what the UI client is doing. We can alleviate it, but the intention is that if the UI asks for a challenge corresponding to a specific token/anchor, let's say eurc
, we check that we allow signing client_domain
proof for it. Only if we allow it, and the operation correspond to what is expected, we sign.
It may be a bit of too defensive and redundant. We can analyze removing it if it doesn't add value. But it's not too much code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should mention that the client_domain
operation is defined by the initial GET request to sep-10
, if it is defined there, it will appear in the operations.
And right now we (the backend) don't control this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, well I guess if we always want to use the client_domain
we can just keep the check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also useful, I think, when we want to also disable some anchor. Yes I also understand we always want to send the client_domain
. It's defensive, but for example right now we are experiencing a failure if we specify the client_domain
with Anclap. So we disable it here and we can be sure it's not a misconfiguration on the frontend.
@@ -7,7 +7,7 @@ const { | |||
STELLAR_EPHEMERAL_STARTING_BALANCE_UNITS, | |||
} = require('../../constants/constants'); | |||
const { TOKEN_CONFIG, getTokenConfigByAssetCode } = require('../../constants/tokenConfig'); | |||
|
|||
const { fetchTomlValues } = require('../helpers/anchors'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not used in this file.
} | ||
|
||
// the web_auth_domain and client_domain operation must be present | ||
if (!hasWebAuthDomain) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to this section, it does not seem to be mandatory that the challenge transaction has a web_auth_domain
entry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand from here that is not optional. You get that idea from step 7?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It sounds contradictory, I saw that. I thought that this is some backward compatible definition, i.e., an older version did not have this. Whoever creates the challenge should add this field in the current version. Whoever checks the challenge should accept also the older challenge without that field.
But who knows. We can just keep it as it is as long as it works with both anchors.
amount: sessionParams.offrampAmount, | ||
}); | ||
let sep24Params; | ||
if (outputToken == 'ars') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would again avoid token specific hard coded logic here, but I understand that this is really specific logic that is only valid for Anclap and we might remove it again once they implement this compliantly (account
is the SEP-24 instead of the SEP-10 account).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, me too. But Anclap, therefore ars
, seems to be the only one requiring the account
on the sep-24
. It seems from our current conversations that it will continue to be this way.
@pendulum-chain/devs I believe I fixed and added the changes requested. A word on Anclap: Sep-10 request:
Sep-10 response:
I believe the error is clear, a client cannot sign a transaction when an account does not exists (ephemeral never exists at this stage of course). Luckily the master + memo + client_domain PR works. The recurrent user identification is being fixed by Anclap. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
Original issue: #225.
This PR also adds partially ARS integration: #172 and #173. Partially because given a UI particularity of Anclap, the ephemeral cannot perform the offramp because it doesn't have ARS token when created.
Main addition
Adds a new endpoint to the signer service, which upon request, will sign the corresponding challenge from the anchor. Also modifies the UI logic to add the extra
client_domain
parameter, and append the signature to the response when required.Security
Although the challenge transaction is sent by the UI, we perform corresponding challenge operations checks.