From 885943cd11d9c2cdb34d656c7a4d73fb0ad25c96 Mon Sep 17 00:00:00 2001 From: Charles Hill Date: Fri, 10 Sep 2021 14:07:43 +0100 Subject: [PATCH] LUD-21: Signed LNURLs --- 21.md | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 1 + 2 files changed, 174 insertions(+) create mode 100644 21.md diff --git a/21.md b/21.md new file mode 100644 index 0000000..0877c24 --- /dev/null +++ b/21.md @@ -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"`: +* `"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. + +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¤cy=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¤cy=EUR +2. Add the authorization key's identifier to the URL's query string as follows: + * https://example.com/lnurl?tag=withdraw&amount=5¤cy=EUR&id=935e30a7 +3. Generate a unique, random nonce and add it to the query string: + * https://example.com/lnurl?tag=withdraw&amount=5¤cy=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¤cy=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¤cy=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¤cy=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¤cy=EUR&id=935e30a7&nonce=d2e3c794&tag=withdraw&signature=80224eed83e03acd0e44760f42b3a7157f549d04cf0160574246e9a87ff9bf8f +2. Separate the query string from the rest of the request URL. Example: + * `amount=5¤cy=EUR&id=935e30a7&nonce=d2e3c794&tag=withdraw&signature=80224eed83e03acd0e44760f42b3a7157f549d04cf0160574246e9a87ff9bf8f` +2. Remove the signature from the query string. Example: + * `amount=5¤cy=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¤cy=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: +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¤cy=EUR&id=935e30a7&nonce=d2e3c794&tag=withdraw", + "signature": "80224eed83e03acd0e44760f42b3a7157f549d04cf0160574246e9a87ff9bf8f" + }, + { + "authorizationKey": { + "id": "4155710c", + "key": "bGAzwLUv1ivWOtARN3pcLV8ry1gdaaAPn2n6wdrKiuY=", + "encoding": "base64" + }, + "payload": "amount=5¤cy=EUR&id=4155710c&nonce=d2e3c794&tag=withdraw", + "signature": "5709dbc00362abbf7ad4da05d9058992b969a3a0c8d771c9310d1ab4738a278e" + }, + { + "authorizationKey": { + "id": "123", + "key": "a plaintext secret", + "encoding": "" + }, + "payload": "amount=5¤cy=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)); +``` diff --git a/README.md b/README.md index 0628625..e7b9a80 100644 --- a/README.md +++ b/README.md @@ -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