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

LUD-21: Signed LNURLs #91

Open
wants to merge 1 commit into
base: luds
Choose a base branch
from
Open
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
173 changes: 173 additions & 0 deletions 21.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
LUD-21: Signed LNURLs
=====================================================

`author: chill117`

---

## Authorize the creation of LNURLs for service

A service may authorize other applications to create LNURLs without direct interaction between the authorized application and the service. This can allow offline hardware devices to generate LNURLs for a service that supports the signing scheme described in this document.

The service must keep a list of authorized keys so that it can check whether a request was signed by an authorized key. The authorized keys list can be empty - in this case no signed requests will be accepted. An authorization key should include a secret key, the encoding of the secret key, and a unique identifier. An example authorization key as a JSON object:
```json
{
"id": "935e30a7",
"key": "e31b5c188346f3a83a7e698486bee48522eed378847126d78dbc030093ea14c7",
"encoding": "hex"
}
```
Possible values for `"encoding"`:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to support different encodings?
I think just going with hex is better to keep the protocol simpler.

Copy link
Contributor Author

@chill117 chill117 Sep 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to support different encodings?
I think just going with hex is better to keep the protocol simpler.

Flexibility. The bleskomat-server, lnurl-node module (JS), lnurl-platformio (C++), and the bleskomat extension (python) for lnbits all support the three different secret key encodings described here.

* `"base64"` - Base64 encoded
* `"hex"` - Hexadecimal encoded
* `""` (empty-string) - Unencoded / plaintext (utf8)

The `"id"` should be a unique identifier so that an individual authorization key may be found by this value.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't key already the identifier?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't key already the identifier?

The identifier is needed to perform the authorization key look-up and signature check in the service side.


An authorization key is to be provided to the authorized application so that it can generate signed URLs.


### Steps to generate a signed URL

These steps are to be done by an authorized application.

1. __Build the base URL__ in the same way that your service would normally less the `k1` or secret value. For example, if your service generates withdraw links like https://example.com/lnurl?tag=withdraw&amount=5&currency=EUR&k1=0aa4a2285fb16207865c87c28bb0d78f42248f3077aba917c2f55b6be51e5e3d where `k1` is the secret that makes the link unique and acts as a secret (or password) which grants access to the URL. The `k1` can be left out here because we are using signing to authorize the creation of the URL in the server. So the example URL here would be as follows:
* https://example.com/lnurl?tag=withdraw&amount=5&currency=EUR
Copy link
Collaborator

@hsjoberg hsjoberg Sep 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like ¤ gets rendered as an HTML entity.
But AFAICT currency param is not the withdraw spec anyway so this could be removed.

Copy link
Contributor Author

@chill117 chill117 Sep 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like ¤ gets rendered as an HTML entity.
But AFAICT currency param is not the withdraw spec anyway so this could be removed.

There are no explicitly required query parameters for the URL that ends up serving an LNURL response object. That's why I made this a simple example using only the tag, amount, and currency. The example URL here is not the callback URL as described in the other LUDs. This is the URL for the initial request, which will contain the callback URL in its response object.

2. Add the authorization key's identifier to the URL's query string as follows:
* https://example.com/lnurl?tag=withdraw&amount=5&currency=EUR&id=935e30a7
3. Generate a unique, random nonce and add it to the query string:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unique is enough, doesn't need to be random. (And actually just incrementing a number by one and storing in persistent memory may be safer - no RNG vulnerabilities possible.)

* https://example.com/lnurl?tag=withdraw&amount=5&currency=EUR&id=935e30a7&nonce=d2e3c794
* 32 bits of random data is probably sufficient entropy to avoid collisions
4. Sort the query string by key (alphabetically). The above example would result in the following:
* `amount=5&currency=EUR&id=935e30a7&nonce=d2e3c794&tag=withdraw`
5. The sorted query string will be referred to as the "payload".
* Note that both the keys and values in the query string must be URL-encoded. The following characters should be __unescaped__: `A-Z a-z 0-9 - _ . ! ~ * ' ( )`. See [encodeURIComponent](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#description) for more details.
6. Sign the payload using the authorization key secret. Signatures are generated using HMAC-SHA256, where the authorization key secret is the key and the payload is the message.
7. Add the signature to the query string. Example:
* `amount=5&currency=EUR&id=935e30a7&nonce=d2e3c794&tag=withdraw&signature=80224eed83e03acd0e44760f42b3a7157f549d04cf0160574246e9a87ff9bf8f`
8. Build the full URL with the signed query string. Example:
* https://example.com/lnurl?amount=5&currency=EUR&id=935e30a7&nonce=d2e3c794&tag=withdraw&signature=80224eed83e03acd0e44760f42b3a7157f549d04cf0160574246e9a87ff9bf8f


### Steps to verify a signed URL

These steps are to be done by the service when it receives a signed LNURL request:

1. Given the following example request URL:
* https://example.com/lnurl?amount=5&currency=EUR&id=935e30a7&nonce=d2e3c794&tag=withdraw&signature=80224eed83e03acd0e44760f42b3a7157f549d04cf0160574246e9a87ff9bf8f
2. Separate the query string from the rest of the request URL. Example:
* `amount=5&currency=EUR&id=935e30a7&nonce=d2e3c794&tag=withdraw&signature=80224eed83e03acd0e44760f42b3a7157f549d04cf0160574246e9a87ff9bf8f`
2. Remove the signature from the query string. Example:
* `amount=5&currency=EUR&id=935e30a7&nonce=d2e3c794&tag=withdraw`
3. Sort the query string by key (alphabetically) in the case that it is not already. Example:
* `amount=5&currency=EUR&id=935e30a7&nonce=d2e3c794&tag=withdraw`
4. The sorted query string will be referred to as the "payload".
5. Use the `id` in the query string to find the authorization key in the service's list of authorization keys.
* If the authorization key is __not__ found, then fail the request.
* If the authorization key is found, then continue with the signature check.
6. Sign the payload using the authorization key secret that was found in the previous step. Signatures are generated using HMAC-SHA256, where the authorization key secret is the key and the payload is the message.
7. Check the signature from the query string against the signature generated by the service.
* If the signature __does not__ match, then fail the request.
* If the signature matches, then continue with the service's normal LNURL flow.

_Optional_ steps to generate a deterministic value to be used as the `k1` or identifier of an LNURL record:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this can be optional. This makes LUD-21 incompatible with the base withdraw spec LUD-03.

Specifically:

  1. Once accepted by the user, LN WALLET sends a GET to LN SERVICE in the form of
 <callback>
     <?|&> // either '?' or '&' depending on whether there is a query string already in the callback
      k1=<k1> // the k1 specified in the response above
     &pr=<lightning invoice> // the payment request generated by the wallet

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's optional because you don't need to deterministically generate the k1 from the signed URL. The service can randomly generate the k1 and use that in the response object. This would mean that your service will accept any and all valid, signed LNURL and never check for re-use. Or you could find some other way to prevent re-use of signed LNURLs.

1. Concatenate the `id` and `signature` values together. Example:
* `935e30a7-80224eed83e03acd0e44760f42b3a7157f549d04cf0160574246e9a87ff9bf8f`
2. Hash the result of the previous step using SHA256. Example:
* `e3c99bc67a12b3cc90cdc9a2604564fea3e54c8529f3fc5166fb92e0f7f5a3f0`
3. The resulting hash can be used as the `k1` value or unique, deterministic identifier for the signed request.
* This can allow your service to prevent or limit the re-use of signed URLs.


## Test vectors

The below test vectors are formatted as a JSON array:
```json
[
{
"authorizationKey": {
"id": "935e30a7",
"key": "e31b5c188346f3a83a7e698486bee48522eed378847126d78dbc030093ea14c7",
"encoding": "hex"
},
"payload": "amount=5&currency=EUR&id=935e30a7&nonce=d2e3c794&tag=withdraw",
"signature": "80224eed83e03acd0e44760f42b3a7157f549d04cf0160574246e9a87ff9bf8f"
},
{
"authorizationKey": {
"id": "4155710c",
"key": "bGAzwLUv1ivWOtARN3pcLV8ry1gdaaAPn2n6wdrKiuY=",
"encoding": "base64"
},
"payload": "amount=5&currency=EUR&id=4155710c&nonce=d2e3c794&tag=withdraw",
"signature": "5709dbc00362abbf7ad4da05d9058992b969a3a0c8d771c9310d1ab4738a278e"
},
{
"authorizationKey": {
"id": "123",
"key": "a plaintext secret",
"encoding": ""
},
"payload": "amount=5&currency=EUR&id=123&nonce=d2e3c794&tag=withdraw",
"signature": "abbd793e08b1fff85ff684639dd0283037a7cfd99b5af8e19fbff8dfb31397dd"
}
]
```

Test vectors generated with the following code to be run with nodejs:
```js
const crypto = require('crypto');
const querystring = require('querystring');

const authorizationKeys = [
{
id: '935e30a7',
key: 'e31b5c188346f3a83a7e698486bee48522eed378847126d78dbc030093ea14c7',
encoding: 'hex',
},
{
id: '4155710c',
key: 'bGAzwLUv1ivWOtARN3pcLV8ry1gdaaAPn2n6wdrKiuY=',
encoding: 'base64',
},
{
id: '123',
key: 'a plaintext secret',
encoding: '',
},
];

const createSignature = function(key, encoding, payload) {
encoding = encoding || 'utf8';
return crypto.createHmac('sha256', Buffer.from(key, encoding)).update(payload).digest('hex');
};

const preparePayload = function(query) {
let sortedQuery = Object.create(null);
// Sort the query object by key (alphabetically).
for (const key of Object.keys(query).sort()) {
sortedQuery[key] = query[key];
}
return querystring.stringify(sortedQuery);
};

const testVectors = authorizationKeys.map(authorizationKey => {
const { id, key, encoding } = authorizationKey;
let query = {
amount: 5,
currency: 'EUR',
nonce: 'd2e3c794',
tag: 'withdraw',
id,
};
const payload = preparePayload(query);
const signature = createSignature(key, encoding, payload);
return {
authorizationKey,
payload,
signature,
};
});

console.log(JSON.stringify(testVectors, null, 4));
```
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ These are all the individual documents describing each small piece of protocol t
| [18](18.md) | Proof-of-payer in `payRequest` protocol. | |
| [19](19.md) | Mutually discoverable pay and withdraw links. | |
| [20](20.md) | Long payment description for pay protocol. | [Blixt][blixt] |
| [21](21.md) | Signed LNURLs | (wallet support not required) |

[blixt]: https://blixtwallet.github.io
[bluewallet]: https://bluewallet.io
Expand Down