SEP: 0007
Title: URI Scheme to facilitate delegated signing
Author: Nikhil Saraf (Lightyear.io / SDF)
Status: Final
Created: 2018-05-07
Updated: 2019-06-11
Version: 1.1.0
This Stellar Ecosystem Proposal introduces a URI Scheme that can be used to generate a URI that will serve as a request to sign a transaction. The URI (request) will typically be signed by the user’s trusted wallet where she stores her secret key(s).
Proposal for a URI Scheme that can be used to generate standardized URIs that will represent a request to sign a specific transaction on the Stellar Network. These URIs will be generated by applications that do not hold secret keys and need to delegate signing to a third-party system that is trusted by the user. These URIs can be interpreted by any application or wallet that complies with this SEP. The signing applications (wallets) should follow the recommended security best practices in this SEP when handling these transactions to be deemed as fully-compliant wallets.
Non-wallet applications want a way to have their users sign a transaction that is generated by it without requiring it to ever see the user’s secret key in any form. Users already have their preferences for a trusted wallet where they are storing their secret keys.
This leaves room for a common protocol that can be implemented by both applications that want transactions to be signed as well as wallets that can sign transactions. The common protocol needs to have the following properties for it to be successful and widely accepted:
- Open and Decentralized - The specification needs to be open in that it can be easily read and implemented by anyone. All the dependencies to implement such a specification should be easily accessible, free of cost, and free from a central authority or service. This will ensure that there is no single point of failure in the system and the system can remain permissionless.
- Standardized - The specification should not require any special handling for specific operations on the network. This will ensure that there is only a nominal amount of effort needed for developers on both ends (URI generation and signing) to implement all operations on the network. It should also be independent of a specific platform and it should be possible to easily implement in any programming language.
- Secure - The specification should not expose secret keys in any form. It should include requirements on how wallets can keep their users secure by ensuring that the user has enough visibility into the transaction that she is about to sign.
- Future-Proof - The specification and its implementation should be designed in a way that allows operations that may be added to the Stellar Network in the future to be easily incorporated into the proposed specification. This will ensure that developers need to make minimal upgrades to their applications and wallets to support additions to the network protocol. The specification should also leave room for expansion to accommodate future use cases that were not anticipated at the time of writing.
A standardized URI Scheme can achieve the goals laid out above. The syntax of the scheme will contain all the information needed to make the necessary payment to the application’s account.
The scheme name will be web+stellar
and should always be followed by a colon to look like this: web+stellar:
(no forward-slashes). The syntax for the URI will look like this: web+stellar:<operation>?<param1>=<value1>&<param2>=<value2>
, where operation
, and the options for the query params are defined below. We have included web+
in the scheme name so it can be handled by web pages (see here for more details on web+
).
The tx
operation represents a request to sign a specific XDR TransactionEnvelope
. The parameters for the tx
op are as follows:
xdr
(required) - A StellarTransactionEnvelope
in XDR format that is base64 encoded and then URL-encoded. If the source account is set and the sequence number is 0 then the URI handler should replace the sequence number before signing.callback
(optional) - If this value is omitted then the URI handler should sign the given XDR and submit it to the network. If the value is present then it should be interpreted as a URL-encoded callback. The URL-encoded callback will be prefixed with its own namespace to denote whether this is aurl
callback or some other form of callback. In the case where it is a url callback (denoted byurl:
) the URI handler should send the signed XDR to this url in aPOST
request withContent-Type
set toapplication/x-www-form-urlencoded
with the data fieldsxdr
containing the signed XDR (URL-encoded). If there are any query params specified in the URL callback then those should be included in the URL when submitting. For now onlyurl
callback types are supported.pubkey
(optional) - Specify which public key you want the URI handler to sign for. Useful with thecallback
parameter above for example with multisig coordination.msg
(optional) - There can be an optionalmsg
query param to indicate any additional information that the website or application wants to show the user in her wallet. The value for this query param should be URL-encoded as well and should not be longer than 300 characters before the URL-encoding. Note that themsg
field is different from thememo
field that is included in a transaction. Themsg
field will not be put on-chain, but thememo
field will be put on-chain.network_passphrase
(optional) - Only need to set if this transaction is for a network other than the public network (URL-encoded).origin_domain
(optional) - A fully qualified domain name that specifies the originating domain of the URI request. Wallets must validate the URI request against the includedsignature
before they display theorigin_domain
to the user. See the Request Signing section for more details.signature
(optional) - A signature of the hash of the URI request (excluding thesignature
field and value itself). Wallets should use theURI_REQUEST_SIGNING_KEY
specified in the domain'sstellar.toml
file to validate this signature. If the verification fails, wallets must alert the user. See the Request Signing section for more details.
Example 1 - Change Trust Operation with callback endpoint specified:
web+stellar:tx?xdr=AAAAAP%2Byw%2BZEuNg533pUmwlYxfrq6%2FBoMJqiJ8vuQhf6rHWmAAAAZAB8NHAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAA%2F7LD5kS42DnfelSbCVjF%2Burr8GgwmqIny%2B5CF%2FqsdaYAAAAAAAAAAACYloAAAAAAAAAAAA&callback=url%3Ahttps%3A%2F%2FsomeSigningService.com%2Fa8f7asdfkjha&pubkey=GAU2ZSYYEYO5S5ZQSMMUENJ2TANY4FPXYGGIMU6GMGKTNVDG5QYFW6JS&msg=order%20number%2024
The pay
operation represents a request to pay a specific address with a specific asset, regardless of the source asset used by the payer. If the payer decides to use a different source asset then the wallet should leverage the path payment operation to achieve this. The parameters for the pay
operation are as follows:
destination
(required) - A valid account ID or payment addressamount
(optional) - Amount that destination will receive. If not specified then the wallet should ask the user to enter the amount before signing. The use case for leaving this out is to support donations of arbitrary amounts.asset_code
(optional) - Asset code (XLM if not present) destination will receiveasset_issuer
(optional) - Account ID of asset issuer (XLM if not present) destination will receivememo
(optional) - Can be a memo to be included in the payment / path payment. Memos of typeMEMO_HASH
andMEMO_RETURN
should be base64 encoded and then URL encoded. Memos of typeMEMO_TEXT
should be URL-encoded.memo_type
(optional) - One ofMEMO_TEXT
,MEMO_ID
,MEMO_HASH
,MEMO_RETURN
. See transaction guide for a description of these values.callback
(optional) - If this value is omitted then the URI handler should sign the given XDR and submit it to the network. If the value is present then it should be interpreted as a URL-encoded callback. The URL-encoded callback will be prefixed with its own namespace to denote whether this is aurl
callback or some other form of callback. In the case where it is a url callback (denoted byurl:
) the URI handler should send the signed XDR to this url in aPOST
request withContent-Type
set toapplication/x-www-form-urlencoded
with the data fieldsxdr
containing the signed XDR (URL-encoded). If there are any query params specified in the URL callback then those should be included in the URL when submitting. For now onlyurl
callback types are supported.msg
(optional) - There can be an optionalmsg
query param to indicate any additional information that the website or application wants to show the user in her wallet. The value for this query param should be URL-encoded as well and should not be longer than 300 characters before the URL-encoding. Note that themsg
field is different from thememo
field that is included in a transaction. Themsg
field will not be put on-chain, but thememo
field will be put on-chain.network_passphrase
(optional) - Only need to set if this transaction is for a network other than the public network (URL-encoded).origin_domain
(optional) - A fully qualified domain name that specifies the originating domain of the URI request. Wallets must validate the URI request against the includedsignature
before they display theorigin_domain
to the user. See the Request Signing section for more details.signature
(optional) - A signature of the hash of the URI request (excluding thesignature
field and value itself). Wallets should use theURI_REQUEST_SIGNING_KEY
specified in the domain'sstellar.toml
file to validate this signature. If the verification fails, wallets must alert the user. See the Request Signing section for more details.
Example 1 - Request for a payment with lumens:
web+stellar:pay?destination=GCALNQQBXAPZ2WIRSDDBMSTAKCUH5SG6U76YBFLQLIXJTF7FE5AX7AOO&amount=120.1234567&memo=skdjfasf&msg=pay%20me%20with%20lumens
Example 2 - Request for a payment with a specific asset:
web+stellar:pay?destination=GCALNQQBXAPZ2WIRSDDBMSTAKCUH5SG6U76YBFLQLIXJTF7FE5AX7AOO&amount=120.123&asset_code=USD&asset_issuer=GCRCUE2C5TBNIPYHMEP7NK5RWTT2WBSZ75CMARH7GDOHDDCQH3XANFOB&memo=hasysda987fs&callback=url%3Ahttps%3A%2F%2FsomeSigningService.com%2Fhasysda987fs%3Fasset%3DUSD
By introducing a namespace to the URI in the form of the operation we are leaving the syntax for this URI Scheme open and flexible for future modification.
The application that generated the request URI should check the Stellar Network directly to confirm receipt of the transaction unless it has specified a callback in the callback
query param. Here are three suggestions for the workflow of how an application can achieve this:
- The application should generate the URI dynamically so it can include a unique ID in the memo field. This unique ID can be used to identify the receipt of a specific payment when deciding whether to move the user onto the next state in the application workflow.
- The application can ask the user for her account ID from which she plans to make the payment. The application can then check for a new payment for the requested amount that is made by the user’s account ID. This is slightly more tedious for the user than (1) suggested above and therefore it is recommended that approach (1) be used if it is possible to generate dynamic payment request URIs.
- Specify the
callback
query param with a dynamically generated unique URL endpoint callback. The endpoint will listen for the signed transaction and submit it to the Stellar Network on behalf of the user. This would allow the listener endpoint to interact with the application as needed to record the payment by the user.
Applications that want to increase trust with the end user should include the origin_domain
param along with a signature
param, as described in the operation sections above. This will allow wallets to validate the URI request against the provided signature
and therefore display this origin_domain
to the user.
This is how applications should create the signature
field:
- Ensure that your domain's stellar.toml file has the field
URI_REQUEST_SIGNING_KEY
with the public key that wallets should use to validate request signatures. - Generate the URI request with the
origin_domain
param set to the application's domain. - Convert the URI request into the
payload
that will be signed. The first 35 bytes of thepayload
are all0
, the 36th byte is4
. Then we concatenate the URI request with the prefixstellar.sep.7 - URI Scheme
(no delimiter) and convert that to bytes to give use the finalpayload
to be signed. - Sign this payload with the application's private signing key corresponding to the
URI_REQUEST_SIGNING_KEY
public key. We will need to convert the signature to base64 and then URL-encode it so we can include it in the URI-Request. Take a look at the sample code here for reference. - Append the URL-encoded base64 signature to the URI Request as the value for the
signature
param. This should be the last param to make it easier for wallets to extract out thepayload
for signature verification.
This is how a wallet should handle the origin_domain
and signature
fields:
- If the
origin_domain
field does not exist then do not display anorigin_domain
to the user; no need to proceed further. - If the
signature
field is missing then do not allow the user to sign the transaction. This is not a valid URI request, display an appropriate message to the user; no need to proceed further. - If the
origin_domain
is not a valid fully qualified domain name then do not allow the user to sign the transaction. This is not a valid URI request, display an appropriate message to the user; no need to proceed further. - Fetch the
stellar.toml
file from the domain:https://<origin_domain>/.well-known/stellar.toml
. If thestellar.toml
file does not exist then do not allow the user to sign the transaction. This is not a valid URI request, display an appropriate message to the user; no need to proceed further. It is recommended that wallets do not cachestellar.toml
files and that they always fetch the lateststellar.toml
files as part of processing signatures so that if signing keys of a domain are ever changed then the wallet can alert the user accordingly using the cachedURI_REQUEST_SIGNING_KEY
for comparison as described below. - Extract out the
URI_REQUEST_SIGNING_KEY
field from thestellar.toml
. If this key does not exist then do not allow the user to sign the transaction. This is not a valid URI request, display an appropriate message to the user; no need to proceed further. Wallets should cache the last usedURI_REQUEST_SIGNING_KEY
for a given domain and only use the cached value to compare it to the latest signing key retrieved, if the latest signing key differs from the cached value for that domain then the wallet must alert the user of this change so they are aware that the signing key has changed which poses a security risk to the user. - Once you have the domain's
URI_REQUEST_SIGNING_KEY
you can use it to verify the URI request against the provided signature. In order to do this, you will need to convert the URI Request into thepayload
as described above. It is this payload that is verified using theURI_REQUEST_SIGNING_KEY
. Take a look at the sample code here for reference. - If the signature verification fails then do not allow the user to sign the transaction. This is not a valid URI request, display an appropriate message to the user; no need to proceed further.
- If the signature verification succeeds then the wallet should display the
origin_domain
to the user so the user knows which domain created this URI request. The user is then free to choose whether she wants to proceed with signing a request that originated from that domain. This is the only point at which a wallet should display theorigin_domain
to the user since it has completely verified the URI Request against the includedsignature
.
Example:
Assume this URI request needs signing (notice that the origin_domain
is part of the request along with other query params, however the signature
param is missing):
web+stellar:pay?destination=GCALNQQBXAPZ2WIRSDDBMSTAKCUH5SG6U76YBFLQLIXJTF7FE5AX7AOO&amount=120.1234567&memo=skdjfasf&msg=pay%20me%20with%20lumens&origin_domain=someDomain.com
Let's use the sample code to sign this URI Request with the private key SBPOVRVKTTV7W3IOX2FJPSMPCJ5L2WU2YKTP3HCLYPXNI5MDIGREVNYC
.
The base64 encoded signature is: JTlGMGzxUv90P2SWxUY9xo+LlbXaDloend6gkpyylY8X4bUNf6/9mFTMJs7JKqSDPRtejlK1kQvrsJfRZSJeAQ==
.
When we URL-encode that, we get: JTlGMGzxUv90P2SWxUY9xo%2BLlbXaDloend6gkpyylY8X4bUNf6%2F9mFTMJs7JKqSDPRtejlK1kQvrsJfRZSJeAQ%3D%3D
. This is the signature.
The complete URI request can be compiled by appending the signature to the original URI Request which gives:
web+stellar:pay?destination=GCALNQQBXAPZ2WIRSDDBMSTAKCUH5SG6U76YBFLQLIXJTF7FE5AX7AOO&amount=120.1234567&memo=skdjfasf&msg=pay%20me%20with%20lumens&origin_domain=someDomain.com&signature=JTlGMGzxUv90P2SWxUY9xo%2BLlbXaDloend6gkpyylY8X4bUNf6%2F9mFTMJs7JKqSDPRtejlK1kQvrsJfRZSJeAQ%3D%3D
In order to verify the signature JTlGMGzxUv90P2SWxUY9xo%2BLlbXaDloend6gkpyylY8X4bUNf6%2F9mFTMJs7JKqSDPRtejlK1kQvrsJfRZSJeAQ%3D%3D
against our URI request, we first separate out the signature
field and value from the URI Request. Then we use what is remaining from the URI Request and verify it against the signature
value. The signature needs to first be URL-unescaped and then decoded from base64 into its byte form before it can be used to verify the remaining portion of the URI request against the public key. If there is no error then we know that the signature is valid and the origin_domain
is the originator of this URI request. Wallets should then display the origin_domain
to the user if signature verification succeeds. If signature verification fails then wallets should disallow the user from signing the URI request and display an appropriate message to alert the user.
Here are some specific attacks that would compromise the security of the user. We have listed out ways in which wallets should handle these situations. This list is not comprehensive and is only a starting point for wallet developers to think about security. We strongly encourage all wallet developers to take some time to think about this and share any learnings with the community. Without further ado, here's the list:
-
Threat: URI Request Modification. This is when an attacker changes part of the URI Request. This is possible in a man-in-the-middle scenario.
Suggested Precaution: If the URI is modified, then the
signature
check will fail and the wallet should not allow the user to sign the transaction and should alert the user of possible tampering with the URI request. If theorigin_domain
andsignature
fields do not exist then this is a red flag! Your wallet should make it harder for users to sign such a request by making them go through additional steps to confirm that they understand the risks of signing a transaction that originated from an unsigned URI request. If theorigin_domain
exists but thesignature
field does not exist then it is not a valid request. An unsigned URI Request is equivalent to usinghttp
vs. signed URI Requests being equivalent to usinghttps
and should be treated as such by wallets from a UI perspective. -
Threat: URI Request Hijacking. It is possible that a man-in-the-middle could completely replace the URI Request with another valid request from a different domain. This would result in all the signature checks passing.
Suggested Precaution: If signature verification succeeds then the wallet should display the
origin_domain
in a prominent position on the same page/view where the user is signing the transaction. This will allow the user to know the referrer of the request and can be useful in mitigating such an attack since the user will be more careful before signing a request from an untrusted domain. Wallets should alert the user if they are transacting with anorigin_domain
for the first time as an additional precaution. -
Threat: The originating website creates a different URI Request compared to what the user was shown on their website (charging a higher amount for the same item, adding additional items to the bill, etc.)
Suggested Precaution: Wallets need to display details of the transaction to be signed, including any
msg
sent by the originating domain. The originating domain should include details of the transaction (items in shopping cart, etc.) in themsg
field. -
Threat: The domain of the originating requestor is compromised and the attacker has updated the
URI_REQUEST_SIGNING_KEY
in the domain'sstellar.toml
to one that they have access to allowing them to create signed URI Requests.Suggested Precaution: Wallets should keep a map/dictionary of
origin_domain
toURI_REQUEST_SIGNING_KEY
and alert the user if there is ever a mismatch. Pinning the domain'sURI_REQUEST_SIGNING_KEY
in this manner can be very effective as long as the originalURI_REQUEST_SIGNING_KEY
was not compromised to begin with. -
Threat: The destination address owned by a trusted domain is compromised.
Suggested Precaution: Wallets should keep track of all destination addresses that the user has paid to in the past and should alert the user if they are trying to pay to an address that they have not seen before. This can be combined with the map/dictionary of
origin_domain
toURI_REQUEST_SIGNING_KEY
. Additionally, wallets should also maintain ablacklist
of addresses which they should update regularly. This will ensure that the community can be proactive about identifying malicious actors on the network. -
Threat: A malicious application tricks the user and registers as the default handler for the
web+stellar
scheme name.Suggested Precaution: If the wallet was the default handler when it was last opened then it is reasonable for the wallet to expect that it is still the default handler. If this is not the case, then the wallet should alert the user and actively re-request to be made the default handler. However, if the user dismisses this alert then the wallet should not actively request to be made the default handler as that can become annoying for the user. This will ensure that the user is alerted if such an attack is ever attempted.
-
Threat: Homograph attack - A malicious domain registers a name that looks similar to another trusted domain and uses this to impersonate the trusted domain's URI requests. For example, replacing
l
(i.e. lowercaseL
) withI
(i.e. uppercasei
) or using Cyrrillic letters instead of Latin letters. Learn more about Homograph Attacks here.Suggested Precaution: Wallets should be very careful about the fonts they use when displaying the
origin_domain
,memo
andmsg
. Some fonts do not clearly distinguish between certain characters so it is easy to confuse these characters. Wallets should use a font that allows to clearly distinguish between characters so that users can clearly identify malicious domains that are pretending to be trusted domains. Consider testing your wallet's UI with characters ranging across Unicode (particularly Cyrillic) and not just ASCII.
For multisig accounts the wallet is responsible for coordinating the collection of signatures and submitting to the network/callback. Supporting multisig accounts would require the wallet to have a backend service to support this coordination or to use a third-party service. Wallets should keep in mind to correctly display details of the transaction and metadata appropriately to all signers even for multisig transactions.
The multisig coordination service should replace/add a callback
to the URI Request using an endpoint in it's own backend service so the signer's wallet can return the signature to it. The multisig service should always sign the URI Request it is forwarding and include it's own domain as the origin_domain
and the orignal origin_domain
as the msg
in addition to any other information already included so each signer can see the original origin_domain
before signing. This is by all means a new URI Request that is sent out to signers. Signers that trust the multisig coordination service and will sign transactions based on that trust only, not based on the msg
included.
The multisig coordination service should handle collating signatures from the signers before submitting it in a single transaction that is signed by all signers to the network or callback endpoint. See the sample code here for reference on how to collate signatures.
Here are suggestion on how to register your wallet to handle the new URI Scheme based on your platform:
- Linux: Creating custom URL handlers in Ubuntu 11.04, 11.10, GNOME 3.0
- Windows: Handle URI activation (on Windows)
- Mac: Make Your Own URL Protocol and Handler
- iOS: Implementing Custom URL Schemes and Simpler reference to implement custom URL Schemes on iOS
- Android: https://stackoverflow.com/a/4085365/1484710
- Web: Navigator.registerProtocolHandler()
The choice to go with this URI Scheme and syntax allows us to achieve some of the goals outlined above in the Motivation section. Here is a description of how this particular specification meets those needs:
- Open and Decentralized - The specification uses a URI Scheme that is not linked to a particular domain or entity. The specification itself is public and the tools needed to create these URIs are also free and publicly available (XDR format, Stellar SDK).
- Standardized - The specification allows creating requests for any transaction that is possible on the Stellar Network. This means that we do not need operation-specific handling, like payments vs. change trust as an example. URI Schemes are generic and not tied to a specific programming language or platform and can therefore be implemented on any platform and in any programming language.
- Secure - The syntax for the URI scheme does not expose any secret keys. To be fully compliant with the specification, wallets are required to display the transaction that the user will sign and also save well-known destination addresses to alert the user if she encounters a new recipient. These protections will give the user increased visibility into the transaction they are signing and greater confidence when using wallets that comply with these specifications.
- Future-Proof - Since the syntax allows for including a Stellar transaction directly it will work for operations that are added to the Stellar Network in the future. This will ensure that existing wallets will continue to function as expected as these new operations are added. Since the syntax includes a namespace we can further expand it to include information that we did not think of at the time when this specification was defined.
When the user opens their preferred wallet that implements this SEP, the wallet will request to handle the web+stellar
URI scheme. If there is already a registered default handler for this URI scheme then the wallet should not request to handle this URI scheme so as to not pose an annoyance to the user. Depending on the platform used by the wallet (web browser, application, mobile) this request will be surfaced to the user and can be highlighted by the wallet. The user can then confirm the application's request, making it the default handler for this URI scheme. From this point on any URIs with the web+stellar
scheme name that are clicked will trigger the preferred wallet to be opened by the user's OS.
When a URI request is clicked, the wallet should parse the URI request and present the user with the details as specified in this SEP when requesting a signature. Under no circumstance should the wallet automatically sign requests without the user's express consent.
Alternative forms of transmitting payment requests, such as QR codes, should follow this URI Scheme so it is compatible with existing wallets. In order to communicate a SEP-0007 payment request via a QR-code you should encode the whole SEP-0007 URI into the image rendered by the QR-code. Thus, on reading the QR-code you will get a SEP-0007 encoded URI as the output without having to do any additional processing. Any error correction should be taken care of by the QR-code generator.
There is no common pattern of usage that are currently employed by the community so there is no issue with backwards compatibility.
There are two parts to the reference implementation (below). The second one (transaction xdr extraction and signing) serves as an effective test case for the first one (URI generation).
The reference implementations only serve to demonstrate how the generated XDR can be signed and submitted to the test network along with some of the other code snippets included. The work of displaying the details of the transaction that the user will sign have not been implemented and has been left for wallet developers to implement. Furthermore, the pattern to start the signing script is not a secure pattern as it would expose the user’s secret key to the command line history and potentially to keyloggers as well and we strongly recommend that you do not use the same pattern in any of your own code. When looking over the reference implementation files please keep in mind that they are not tested and are designed only to serve as a Proof-Of-Concept for this SEP specification and are not meant to be used for anything other than that.
- Reference Implementation to generate an XDR
Transaction
for a payment operation and sign it - Sample code to create the
signature
field for the URI request and verify it - Sample code to collate signatures for a multi-signature transaction using base64-encoded signed transaction XDRs
- Sample web handler demonstrates how to parse the payment request
xdr
for thetx
operation