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

Merge the v1.0 protocol changes #24

Merged
merged 15 commits into from
Mar 25, 2024
Merged
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
74 changes: 49 additions & 25 deletions umad-02-keys-and-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Keys

Before VASPs start sending and receiving UMA payments, they need to generate keys which are be used to authenticate
Before VASPs start sending and receiving UMA payments, they need to generate keys which will be used to authenticate
themselves to other VASPs and to receive and decrypt encrypted blobs for sensitive information (like payment and Travel
Rule data).

Expand All @@ -11,7 +11,7 @@ to create secp256k1 keys using openssl, run:

```bash
# Generate a secp256k1 key:
$ openssl ecparam -genkey -name secp256k1 -out ec_key.pem -param_enc explicit
$ openssl ecparam -genkey -name secp256k1 -out ec_key.pem

# Print out the key data:
$ openssl ec -in ec_key.pem -noout -text
Expand All @@ -25,42 +25,66 @@ section.

## Public Key Exchange

VASPs expose their public keys to other VASPs by responding to `GET` requests at the endpoint
`https://<vaspdomain>/.well-known/lnurlpubkey`. This endpoint returns a JSON object with the following structure:
The UMA protocol relies on X.509 certificates for public key exchange among VASPs. VASPs expose their certificates to
other VASPs by responding to `GET` requests at the endpoint `https://<vaspdomain>/.well-known/lnurlpubkey`. This
endpoint returns a JSON object with the following structure:

```json
{
// Used to verify signatures from VASP1. Hex-encoded secp256k1 pub key string.
"signingPubKey": string,
// Used to encrypt TR info sent to VASP1. Hex-encoded secp256k1 pub key string.
"encryptionPubKey": string,
// [Optional] Sec since epoch at which these pub keys must be refreshed.
// They can be safely cached until this expiration.
// Used to verify signatures from VASP1. List of certificates (hex-encoded X.509 DER) ordered from leaf to root.
"signingCertChain": string[],
// Used to encrypt TR info sent to VASP1. List of certificates (hex-encoded X.509 DER) ordered from leaf to root.
"encryptionCertChain": string[],
// [Optional] Sec since epoch at which these certificates must be revalidated or refreshed.
// If not specified, the certificates will not be cached.
"expirationTimestamp": number
}
```

VASPs can also use this endpoint to refresh their keys by returning a new set of keys with a new expiration timestamp.
This is useful if a VASP's keys are compromised or if they want to rotate their keys for security reasons. When receiving
a new set of keys, VASPs can cache them until the expiration timestamp.
If a VASP trusts the party that they wish to transact with, typically established through prior communication,
self-signed certificates are a suitable solution for key exchange. VASPs can create self-signed certificates wrapping
the public keys generated above using common tools such as `openssl`, and expose these certificates to counterparties
via the public API outlined above. It is important to note that self-signed certificates don't provide a good
revocation mechanism, so it is recommended to use a short caching duration (on the order of a few minutes) to minimize
the risk of key compromise. To generate a self-signed X.509 certificate wrapping the public key generated above, run:

```bash
# Generate an x509 certificate from your existing ec_key.pem above:
$ openssl req -new -x509 -key ec_key.pem -sha256 -nodes -out ec_crt.crt -days <expiration in days>

# Print out the PEM representation of the certificate:
$ openssl x509 -in ec_crt.crt -outform PEM
```

Because the `/.well-known/lnurlpubkey` endpoint is hosted directly on the VASP's domain, it is easy for other VASPs to
verify that the keys they receive are actually from the VASP they are trying to communicate with. It does, however, imply
trust in the VASP's domain and DNS. As an additional security measure, VASPs can also verify the authenticity of the
keys they receive by communicating with a **VASP Identity Authority**, a trusted 3rd party who maintains a mapping from
VASP domains to public keys. This step is optional and any VASP ID Authority will provide APIs or interfaces separate
from UMA.
verify that the keys they receive are actually from the VASP they are trying to communicate with. It does, however,
imply trust in the VASP's domain and DNS. As an additional security measure, VASPs can also verify the authenticity of
the certificates they receive by communicating with a **VASP Identity Authority**, a trusted 3rd party who maintains a
mapping from VASP domains to certificates. This step is optional and any VASP ID Authority will provide APIs or
interfaces separate from UMA.

A VASP can inspire more trust from its counterparties by retrieving and using a certificate signed by a trusted VASP ID
Authority. When a VASP receives a signed certificate, they can check if it is signed by an authority that they trust,
and hence, trust the underlying VASP. VASPs have the ability to invalidate their certificates in the case of key
compromise or security-related key rotations. The issuing ID authority must keep track of certificates that are
revoked, and provide this information to counterparties via Certificate Revocation Lists (CRLs) or an Online
Certificate Status Protocol (OCSP) server. The URLs for accessing an ID authority's CRL/OCSP can be found inside the
certificate, and VASPs should periodically check the validity of the certificates they receive to ensure compliance and
security. In the event that the counterparty's certificate is revoked, the VASP can request a new set of certificates
and validate them. Optionally, the counterparty can specify an expiration timestamp at which the VASP is required to
revalidate the certificates, in addition to periodic validation.

## Authentication

Some messages in the UMA protcol must be signed by the VASP who created the message using ECDSA and the secp256k1 keys
Some messages in the UMA protocol must be signed by the VASP who created the message using ECDSA and the secp256k1 keys
as described above. Signatures are created using a VASP's private signing key. The signature is then verified by the
receiving VASP using the sending VASP's `signingPubKey`. The signature is included in the message itself, along with the
sending VASP's domain if needed. The receiving VASP can then verify the signature using the public key and ensure that
the message was not tampered with.
receiving VASP using the sending VASP's signing public key from the `signingCertificate`. The signature is included in
the message itself, along with the sending VASP's domain if needed. The receiving VASP can then verify the signature
using the public key and ensure that the message was not tampered with.

## Encryption

VASPs encrypt sensitive information like payment and Travel Rule information using the receiving VASP's `encryptionPubKey`
via [ECIES](https://cryptobook.nakov.com/asymmetric-key-ciphers/ecies-public-key-encryption). The receiving VASP can
then decrypt the data using their private encryption key only when required for compliance reasons.
VASPs encrypt sensitive information like payment and Travel Rule information using the receiving VASP's encryption
public key from the `encryptionCertificate` via
[ECIES](https://cryptobook.nakov.com/asymmetric-key-ciphers/ecies-public-key-encryption). The receiving VASP can then
decrypt the data using their private encryption key only when required for compliance reasons.
40 changes: 26 additions & 14 deletions umad-04-lnurlp-response.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The response to the LNURLP request is an extension of LNURL's [LUD-06](https://github.com/lnurl/luds/blob/luds/06.md).
It also utilizes the payer data spec as described in [LUD-18](https://github.com/lnurl/luds/blob/luds/18.md) and a
slightly modified version of the local currency spec proposed in [LUD-21](https://github.com/lnurl/luds/pull/207).
slightly modified version of the local currency spec proposed in [LUD-21](https://github.com/lnurl/luds/pull/251).
The full structure of the LNURLP response is:

```raw
Expand All @@ -20,8 +20,6 @@ The full structure of the LNURLP response is:
"code": string, // eg. "PHP",
"name": string, // eg. "Philippine Pesos",
"symbol": string, // eg. "₱",
"minSendable": number,
"maxSendable": number,
// Estimated millisats per "unit" (eg. 1 cent in USD). A double-precision floating point number.
"multiplier": number,
// Number of digits after the decimal point for display on the sender side, and to add clarity around what the
Expand All @@ -31,6 +29,14 @@ The full structure of the LNURLP response is:
// means. For example, if the currency is "BTC" and the multiplier is 1000, really we're exchanging in SATs, so
// `decimals` would be 8.
"decimals": number,
// The inclusion of a convertible field implies the receiving VASP can quote and guarantee a price for a given
// currency.
"convertible": {
// Minimum and maximium amounts the receiver is willing/able to convert to this currency in the smallest unit of
// the currency. For example, if the currency is USD, the smallest unit is cents.
"min": number, // 64-bit integer (long/int64)
"max": number, // 64-bit integer (long/int64)
}
},
],
// Required data about the payer. See LUD-18 for details.
Expand All @@ -47,8 +53,8 @@ The full structure of the LNURLP response is:
"kycStatus": KycStatus, // [enum] KYC state indicating whether the receiver is a KYC'd customer of VASP2.
"signature": string, // hex encoded
"signatureNonce": string,
"signatureTimestamp": number // secs since epoch
"receiverIdentifier": string // The identity of the receiver at VASP2
"signatureTimestamp": number, // secs since epoch
"receiverIdentifier": string, // The identity of the receiver at VASP2
},
"umaVersion": "1.0", // The UMA protocol version that will be used for this transaction.
"tag": "payRequest",
Expand All @@ -67,10 +73,12 @@ Here are some additional examples of the `currencies` field to illustrate how th
"code": "USD",
"name": "US Dollars",
"symbol": "$",
"minSendable": 1,
"maxSendable": 1000000, // 1M
"multiplier": 23400,
"decimals": 2
"decimals": 2,
"convertible": {
"min": 1,
"max": 1000000
},
}
```

Expand All @@ -85,10 +93,12 @@ invoice for 13,923,000 millisats (595 * 23,400) plus applicable conversion fees.
"code": "BTC",
"name": "Bitcoin",
"symbol": "₿",
"minSendable": 1,
"maxSendable": 100000000, // 100M
"multiplier": 1000,
"decimals": 8
"decimals": 8,
"convertible": {
"min": 1,
"max": 100000000
},
},
```

Expand All @@ -103,10 +113,12 @@ Lightning invoice for 10,000 millisats (10 * 1,000) plus applicable conversion f
"code": "USDC",
"name": "USDC",
"symbol": "USDC",
"minSendable": 1000000, // 1M
"maxSendable": 1000000000000, // 1T
"multiplier": 2.34,
"decimals": 6
"decimals": 6,
"convertible": {
"min": 1000000, // 1M
"max": 1000000000000 // 1T
},
}
```

Expand Down
88 changes: 82 additions & 6 deletions umad-05-payreq-request.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,92 @@ The body of the request is a JSON object with the following fields:
// See LUD-18 for details.
<json payerdata>
},
// An int64 - This is the amount in the smallest unit of the specified receiving currency (eg. cents for USD).
"amount": number,
// The currency code of the receiving currency (eg. "USD"). This must be one of the currencies returned in the
// LNURLP response.
"currency": string,
// An amount (int64) followed optionally by a "." and the sending currency code. For example: "100.USD" would send
// an amount equivalent to $1 USD. Note that the amount is specified in the smallest unit of the specified
// currency (eg. cents for USD). Omitting the currency code will default to specifying the amount in millisats.
"amount": string,
// The currency code of the receiving currency (eg. "USD") to which the receiving VASP will convert into when the
// transaction completes. This must be one of the currencies returned in the LNURLP response, and it must have
// been a currency with a "convertible" field.
"convert": string,
// The UMA protocol version that will be used for this transaction. See [UMAD-08](/umad-08-versioning.md).
"umaVersion": "1.0"
"umaVersion": "1.0",
"payeeData": {
"compliance": { "mandatory": boolean },
"name": { "mandatory": boolean },
"identifier": { "mandatory": boolean },
"countryCode": { "mandatory": boolean },
... All fields optional and more fields may be negotiated. See [LUD-22](https://github.com/lnurl/luds/pull/252)
},
}
```

## Currency field examples

The currency spec here is as specified by [LUD-21](https://github.com/lnurl/luds/pull/251), with the caveat that UMA's
payreq request uses a POST and JSON body instead of a GET request with query parameters. Please see the spec for more
examples. As it pertains to UMA, there are two main UX cases to accommodate:

1. **The sender wants to send exactly a certain amount in the receiving currency.**

For example, if a user in the US is paying for some goods or services in Europe, they might need to send *exactly* some
amount in euros. In this case, the sender would enter the amount in the receiving currency. Fields specified by the
sending VASP in the payreq request would look like:

```json
{
"amount": "100.EUR",
"convert": "EUR",
// ... other fields
}
```

This informs the receiving VASP to construct a Lightning invoice which will be converted to 100 euros for their user. This
should include the conversion rate and any fees in the invoice itself to ensure that the receiver gets exactly 100 euros.

1. **The sender wants to send exactly a certain amount in their own currency.**

For example, the sending user has $100 USD and they want to send exactly that amount to their family in Mexico. They would
enter the amount in their own currency. However, their own sending VASP is responsible for the onramp from their sending
currency to bitcoin. The sending VASP can guarantee that conversion rate to their user out-of-band of the UMA protocol.
For example, maybe they've agreed that for $100, they will give the user exactly 191,000 satoshis. Fields specified by
the sending VASP in the payreq request would then look like:

```json
{
"amount": "191000000", // 191,000,000 millisats, so the currency code is omitted.
"convert": "MXN",
// ... other fields
}
```

This informs the receiving VASP to construct a Lightning invoice for exactly 191,000 satoshis and to give their receiving
user the equivalent in Mexican pesos according to their agreed-upon conversion rate. This allows the sender to lock in
the amount they want to send in their own currency.

## Payee Data

The `payeeData` field is optional and is used to request additional information about the receiving user. The `mandatory`
field in each subfield indicates whether the receiving VASP is required to provide that information to proceed with the transaction.
See [LUD-22](https://github.com/lnurl/luds/pull/252) for more details. Note that the receiving VASP may choose to avoid sending
any payee identity information for privacy reasons, which may cause the payment to fail if the sending VASP requires it.
For that reason, the sender SHOULD NOT require any payee identity information to be sent by the receiver unless it is
absolutely necessary.

### Common Payee Data Fields

The following is a non-exhaustive list of common payee data fields that *may* be requested by the sender:

- `name`: The full name of the receiving user.
- `identifier`: The canonical receiving UMA address of the receiver.
- `countryCode`: The ISO 3166-1 alpha-2 country code of the receiving user.
- `email`: The email address of the receiving user.
- `accountNumber`: The account number of the receiving user at the receiving VASP.

Note that this struct is extensible, so any field can be added as long as it is agreed upon by both VASPs.

## Payer Data

The `payerData` field is a JSON object that contains information about the payer as described in
[LUD-18](https://github.com/lnurl/luds/blob/luds/18.md). There's also an UMA-specific payerdata field - `compliance`.
Its structure is:
Expand Down
46 changes: 34 additions & 12 deletions umad-06-payreq-response.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,16 @@ The full structure of the LNURLP response is:
"pr": string,
// Empty for legcy LNURL reasons.
"routes": [],
"compliance": {
// Public key of the recipient node for pre-screening.
"nodePubKey": string,
// A list of the expected UTXOs over which the receiver may receive the transaction (receiver's channels).
"utxos": string[],
// A url which the sending VASP should call on transaction completion to notify the receiving VASP of
// the utxos used to complete the transaction. See [UMAD-07](/umad-07-post-tx-hooks.md).
"utxoCallback": string
},
"paymentInfo": {
"converted": {
// The amount that the receiver will receive in the receiving currency not including fees. The amount is specified
// in the smallest unit of the currency (eg. cents for USD).
"amount": number, // int64
// The currency code of the receiving currency (eg. "USD"). This should match the requested currency in the payreq
// request.
"currencyCode": string,
// Millisats per "unit" of the receiving currency (eg. 1 cent in USD). A double-precision floating point number.
// In this context, this is just for convenience. The conversion rate is also baked into the invoice amount itself.
// `invoice amount = amount * multiplier + exchangeFeesMillisatoshi`
// `invoice amount = amount * multiplier + fee`
"multiplier": number,
// Number of digits after the decimal point for the receiving currency. For example, in USD, by convention, there are
// 2 digits for cents - $5.95. In this case, `decimals` would be 2. This should align with the currency's `decimals`
Expand All @@ -34,7 +28,27 @@ The full structure of the LNURLP response is:
"decimals": number,
// The fees charged (in millisats) by the receiving VASP to convert to the target currency.
// This is separate from the multiplier rate.
"exchangeFeesMillisatoshi": number
"fee": number
},
"payeeData": {
"compliance": {
// Public key of the recipient node for pre-screening.
"nodePubKey": string,
// A list of the expected UTXOs over which the receiver may receive the transaction (receiver's channels).
"utxos": string[],
// A url which the sending VASP should call on transaction completion to notify the receiving VASP of
// the utxos used to complete the transaction. See [UMAD-07](/umad-07-post-tx-hooks.md).
"utxoCallback": string,
// The receiving VASP's signature over sha256_hash(<sender UMA> (eg. [email protected]) + <receiver UMA> (eg. [email protected]) +
// signatureNonce + signatureTimestamp)
"signature": string,
"signatureNonce": string,
"signatureTimestamp": number, // in seconds since epoch
},
"name": string,
"identifier": string,
"countryCode": string,
...other fields may be included if supported by receiver and requested by sender
},
"umaVersion": "1.0",
}
Expand All @@ -47,3 +61,11 @@ a hard cap on the conversion rate validity.
As described in [UMAD-05](/umad-05-payreq-request.md), the `nodePubKey` and `utxos` fields are used for pre-screening
by the sending VASP. The `utxoCallback` field is used by the receiving VASP to notify the sending VASP of the utxos
used to complete the transaction. See [UMAD-07](/umad-07-post-tx-hooks.md) for details.

Each key in the `payeeData` JSON object should correspond to a requested `payeeData` field from the `payeeData` record
received from the sender in the payreq request. The receiving VASP can send any of the payee id kinds if they are listed
in the `payeeData` record. But if any is marked as `"mandatory": true` then receiver MUST send them or otherwise
not proceed with the payment flow. The receiving VASP SHOULD NOT send payee identity types omitted in payeeData record,
none at all if the record is not present. Note that LUD-22 can be used in conjunction with LUD-18 to allow the payer to
request that the payee provide identitifying information (which can be optionally verified by SERVICE) before sharing
payee identity information.
Loading
Loading