Create a gist now

Instantly share code, notes, and snippets.

@sharafian /README.md Secret
Last active Feb 8, 2017

What would you like to do?
Yet Another SPSP Proposal

Yet Another SPSP Proposal

Terms

  • SPSP - "simple payment setup protocol"
  • KEP - "key exchange protocol", formerly known as SSP ("shared secret protocol"). (it lets you KEP sending payments) (name subject to further change)

Flow

This protocol MUST use HTTPS.

(Optional) Webfinger Lookup

GET /.well-known/webfinger?resource=acct:alice@example.com HTTP/1.1
Host: example.com
Accept: application/json

HTTP/1.1 200 OK
Content-Type: application/json

{
  "subject": "acct:alice@example.com",
  "links": [
    {
      "rel": "https://interledger.org/rel/spsp/v1",
      "href": "https://example.com/api/propose/alice"
    }
  ]
}

Proposal ("Establish"? "Proposition"?)

GET /api/propose/alice HTTP/1.1
Host: red.ilpdemo.org
Accept: application/json

HTTP/1.1 200 OK
Content-Type: application/json

{
  "shared_secret": "ZiI6-BbXMNud44kz2zIjlS_mztonBC0AKbeQqLmsVik",
  "destination_account": "example.main.alice.300e3f06-7501-46e9-9a5c-4ebba322c7de",
  "maximum_destination_amount": "20",
  "minimum_destination_amount": "0.01",
  "receiver": {
    "name": "Alice",
    "type": "payee",
    "image_url": "https://example.com/api/receivers/alice/profile_pic.jpg"
  }, 
  "ledger": {
    "currency_code": "USD",
    "currency_symbol": "$",
    "amount_scale": "4",
    "amount_precision": "10"
  }
}

If the receiver wants to present an invoice, this object would look more like:

{
  "shared_secret": "ZiI6-BbXMNud44kz2zIjlS_mztonBC0AKbeQqLmsVik",
  "destination_account": "example.main.alice.invoices.3463266",
  "maximum_destination_amount": "15.76", // minimum and maximum amount leave only one option
  "minimum_destination_amount": "15.76"
  "receiver": {
    "name": "Invoice for order number #346266",
    "type": "invoice",
    "image_url": "https://example.com/api/invoices/3463266/items.jpg"
  }, 
  "ledger": {
    "currency_code": "USD",
    "currency_symbol": "$",
    "amount_scale": "4",
    "amount_precision": "10"
  }
}

Generate Payment

Done via KEEP. Sender converts payment details to canonical form, and then HMAC's them with payment.shared_secret. The sender then sends this payment to payment.address.

const proposal = yield request.get('https://example.com/api/propose/alice')

const transfer = {
  id: uuid(),
  amount: '15.76',
  account: proposal.payment.account,
  // ...
}

// something along these lines
const hmac = crypto.hmac('sha256', proposal.payment.shared_secret)
hmac.update(canonicalJSON.stringify(transfer)
const condition = hmac.digest().toString('base64')

yield plugin.sendTransfer(Object.assign({ condition }, transfer)

I don't like that there are 3 objects returned. Also weren't we getting rid of the receiver "types"?

justmoon commented Feb 1, 2017

https://interledger.org/rel/propose

I'd say the rel would be https://interledger.org/rel/spsp. SPSP shouldn't need any other endpoints. If it ever does, it could be https://interledger.org/rel/spsp/[whatever]

I'd also be open to versioning in this case, e.g. https://interledger.org/rel/spsp/1.0.

Otherwise looks great! Thanks for the quick write-up.

Also, @justmoon had the idea of making the HMAC take as the message something like the string (where each line is separated by a newline character):

ilp-payment
Amount: 15.76
Account: example.main.alice.invoices.3463266
Data: <base64 data>

Instead of using canonical JSON

Owner

sharafian commented Feb 1, 2017

@emschwartz my reasoning on splitting into objects and keeping the type is that it makes it easier for us to say which fields are required and which ones aren't. For example, we can say

  1. The payment object contains required payment information, and all fields are required.
  2. The receiver object contains additional biographical information, and all fields are optional.
  3. The ledger object contains required ledger metadata, and all fields are required.

@justmoon I'll fix that. I like the idea of something like https://interledger.org/rel/spsp/v1

justmoon commented Feb 1, 2017

"account": "example.main.alice.300e3f06-7501-46e9-9a5c-4ebba322c7de",

Let's use destination_account here - I'm trying to gently push us towards using destination_account and destination_amount as the names in the ILP packet and anywhere else that those specific values appear.

(I would propose we use source_account and destination_account consistently on the ILP and transport layers. On the ledger layer, I would deliberately not use the same nomenclature, because transfers will contain both values and it'll be confusing to have a transfer.destination_account and a transfer.data.ilp.destination_account. It'd be better to have a transfer.credit_account (Common Ledger API) or even just transfer.to and a transfer.data.ilp.destination_account. The idea would be that the word "destination" kind of sounds like a longer journey - interledger - vs credit/debit which perhaps more likely conjures the image of a local atomic transfer.)

Also weren't we getting rid of the receiver "types"?

True! For that, we would basically have to deconstruct the functionality of the invoice type. Here's my attempt:

  • Some way to specify how much the invoice is for: possibly min_amount/max_amount where both are equal for an invoice.
  • Some way to provide additional information like itemization, order status, package tracking ID etc. - perhaps a free-form description field? Maybe a related_link?
  • Some way to indicate that the invoice is already paid - perhaps a simple set of error codes à la HTTP: "408 Already Paid"
  • Some way to provide branding (maybe) - image_url should work for that

"key exchange establishment protocol"

Don't think it should be called both "exchange" and "establishment". Those are kind of redundant.

I'd propose we think about this slightly differently which would allow us to be consistent with some REST principals and not lose any functionality. I'd consider consistency with principals/conventions as useful as simplicity because it makes a developers life easier. See https://en.wikipedia.org/wiki/Principle_of_least_astonishment.

I see a payment as a sub-resource of a receiver. What we are defining in the protocol is that a sender can request a receiver to create a payment. It's perfectly okay to submit this request with no parameters and for the reciever to respond with the complete payment.

So I propose that if a user wishes to fetch the details of a receiver then they should use a GET but if you're skipping that step then go straight to POST on receivers/{receiver_id}/payments.

If the payment is created and can be fetched via a GET (like and invoice) then the response should return a 201 Created response code and a Location header with the URL of the new payment (receivers/{receiver_id}/payments/{payment_id}).

If the payment resource is temporary (i.e. The receiver is not persisting it anywhere, e.g. for a payee) then the response should return a 202 Accepted response code and no Location header.

I'd also solve the issue of invoice vs person implicitly through the max and min amounts.

Concrete Proposal:

  • Make the endpoint name /api/receivers/alice/payments/{id} (payment and ledger are sub-resources of receiver) and allow some input via the POSTed resource:

    • amount - proposed amount to send
    • shared_secret - allow the sender to propose the secret
    • setup_protocol - (optional, default to "shared_secret") but allow others like "interledger_payment_request"
  • Don't make max and min the same, just use amount. i.e. It's either a range or a value.

Example 1 - Payee with all defaults:

  • Empty resource POST'ed
  • Transient payment resource created
  • No Location header
  • Uses Shared Secret setup protocol
POST /api/receiver/alice/payments HTTP/1.1
Host: red.ilpdemo.org
Accept: application/json
{}

HTTP/1.1 202 Accepted
Content-Type: application/json
Location: /api/receiver/alice/payments/300e3f06-7501-46e9-9a5c-4ebba322c7de

{
  "id": "300e3f06-7501-46e9-9a5c-4ebba322c7de",
  "shared_secret": "ZiI6-BbXMNud44kz2zIjlS_mztonBC0AKbeQqLmsVik",
  "account": "example.main.alice.300e3f06-7501-46e9-9a5c-4ebba322c7de",
  "maximum_amount": "20",
  "minimum_amount": "0.01",
  "receiver": {
    "name": "Alice",
    "image_url": "https://example.com/api/receivers/alice/profile_pic.jpg"
  }, 
  "ledger": {
    "currency_code": "USD",
    "currency_symbol": "$",
    "amount_scale": "4",
    "amount_precision": "10"
  }
}

Example 2 - Invoice use case:

  • Sender specifies ID for payment (provided via some other channel)
  • Sender can also specify amount allowing the receiver to decide if they will accept a partial payment. The receiver could respond with an error such as 409 Conflict if the sender attempts to create a payment with an amount that is not accepted by the receiver.
  • Specific amount returned
POST /api/receiver/alice/payments/300e3f06-7501-46e9-9a5c-4ebba322c7de HTTP/1.1
Host: red.ilpdemo.org
Accept: application/json
{
  "id" : "300e3f06-7501-46e9-9a5c-4ebba322c7de",
  "amount" : "20"
}

HTTP/1.1 201 Created
Content-Type: application/json

{
  "id": "300e3f06-7501-46e9-9a5c-4ebba322c7de",
  "shared_secret": "ZiI6-BbXMNud44kz2zIjlS_mztonBC0AKbeQqLmsVik",
  "account": "example.main.alice.300e3f06-7501-46e9-9a5c-4ebba322c7de",
  "amount": "20",
  "receiver": {
    "name": "Alice",
    "image_url": "https://example.com/api/receivers/alice/profile_pic.jpg"
  }, 
  "ledger": {
    "currency_code": "USD",
    "currency_symbol": "$",
    "amount_scale": "4",
    "amount_precision": "10"
  }
}

Example 3 - IPR use case:

  • Receiver returns an IPR url instead of a shared secret
  • Sender could provide a requested expiry, amount or any other IPR relevant field (supported condition types?)
POST /api/receiver/alice/payments/ HTTP/1.1
Host: red.ilpdemo.org
Accept: application/json
{
  "setup_protocol" : "interledger_payment_request"
}

HTTP/1.1 202 Accepted
Content-Type: application/json

{
  "id": "300e3f06-7501-46e9-9a5c-4ebba322c7de",
  "ipr_url": "/api/receiver/alice/payments/300e3f06-7501-46e9-9a5c-4ebba322c7de/request",
  "account": "example.main.alice.300e3f06-7501-46e9-9a5c-4ebba322c7de",
  "maximum_amount": "20",
  "minimum_amount": "0.01",
  "receiver": {
    "name": "Alice",
    "image_url": "https://example.com/api/receivers/alice/profile_pic.jpg"
  }, 
  "ledger": {
    "currency_code": "USD",
    "currency_symbol": "$",
    "amount_scale": "4",
    "amount_precision": "10"
  }
}

POST /api/receiver/alice/payments/300e3f06-7501-46e9-9a5c-4ebba322c7de/request HTTP/1.1
Host: red.ilpdemo.org
Accept: application/json
{
  "amount" : "10",
  "sender_reference" : "456e3f06-7501-46e9-9a5c-4ebba322c7de",
  "memo" : { }  
}

HTTP/1.1 201 Created
Content-Type: application/json

{
  "address" : "example.main.alice.300e3f06-7501-46e9-9a5c-4ebba322c7de",
  "amount" : "10",
  "expires_at" : "2017-02-02 20:00:00.7866",
  "condition" : "ni:///sha-256;iygwef76twugr97ptywef7tpweygp97g?..."
}

I really don't like the idea of having an option of whether to use the shared secret or IPR method. I think we should pick one and go with it. If there is a good use case for whichever "transport layer" protocol we don't pick for the thing formerly known as SPSP, it'll get used elsewhere.

I also don't think we should allow the sender to specify a shared secret. What benefit does this offer? Seems like it just adds another potential flow for no reason.

In light of these two ideas, I don't think we need a POST or PUT endpoint. In Ben's example I think the field name "payment" was confusing, but that is not actually referring to a single payment but rather the details for setting up payments in the future.

I like the idea of just having a single HTTP GET endpoint where you fetch the details that will be used to set up subsequent payments. The sender can hold on to the shared secret provided by the receiver or ask for a new one each time. It's a straightforward flow, and I was convinced that it's a better one than the flow involving IPR.

adrianhopebailie commented Feb 2, 2017

I like the idea of just having a single HTTP GET endpoint where you fetch the details that will be used to set up subsequent payments.

I think we need to go back to understanding the use cases we're trying to support here:

  • How does this support the invoice use case where the receiver provides the sender with both their URL and the invoice reference?
  • How does this support any use case where the condition is defined by the receiver?
  • How does a receiver respond to this if they're processing numerous payments in parallel and have a receive limit that will be exceeded if all of them send the max_amount?

How does this support the invoice use case where the receiver provides the sender with both their URL and the invoice reference?

That's a simple GET

How does this support any use case where the condition is defined by the receiver?

It doesn't. The argument behind switching from IPR to SSP is that that is not an important feature (it's not really a use case).

How does a receiver respond to this if they're processing numerous payments in parallel and have a receive limit that will be exceeded if all of them send the max_amount?

They have to check when the money comes in anyway. If you check during the "setup" phase, you still need to check when money comes in because you aren't going to keep track of how much money you think might be in flight at that moment at setup and reject things based on that. This was one of Stefan's arguments that convinced me. Since you need to do this check right before fulfilling the payment anyway you might as well only do it then.

justmoon commented Feb 3, 2017

Also, @justmoon had the idea of making the HMAC take as the message something like the string (where each line is separated by a newline character):

One thing I would do differently from your example is dropping the base64 encoding for the body and handling the content like HTTP instead (two newlines to indicate start of body; raw binary is allowed.) You could even add a content type:

ilp-payment
Destination-Amount: 15.76
Destination-Account: example.main.alice.invoices.3463266
Content-Type: text/plain

Hi, I'm the attached data - I could be binary and it would be fine!

There is another issue: A downside of removing the Setup call (and one reason we originally added it) is that without it, we can no longer just rely on the HTTPS channel to transfer the memo privately.

To fix that, it seems we could use 256-bit AES-CTR using the shared secret¹. The HMAC we already include (i.e. the condition) provides message authentication. We may want to run the HMAC over the ciphertext, not the plaintext.

Before everyone yells "AES is not simple!" consider that it's a tradeoff. The additional round trip is more complex for the receiver who now has to implement three handlers (Query, Setup and Payment) instead of two (Query and Payment). So we're trading simplicity from the implementer of SSP's perspective for simplicity from the receiver's perspective. Arguably the receiver is more important.

AES would undoubtedly add bulk to the ilp module. Not a lot though, for instance aes-js is <800 lines with zero dependencies.

I'm still pretty firmly for taking Setup out, but I understand if folks would want more discussion after learning about this trade-off.

¹ We currently derive an HMAC key from the shared secret. For encryption, we'd derive an encryption key the same way.

justmoon commented Feb 3, 2017

Since you need to do this check right before fulfilling the payment anyway you might as well only do it then.

And just in case someone is thinking of saying that the Setup should be like a prepare where you essentially place a "hold" on the limit - that would make the receiver stateful which would be an even bigger disadvantage compared to the Setup-less flow. (In terms of complexity/performance.)


@sharafian: One thing I'd like to see in your proposal is an example of sender information as query parameters attached to the proposition step, e.g. sender_account


Another thought: We've talked about dropping the query step and packing it into the webfinger step. We agreed that that doesn't seem like a good idea. But is that also true for dropping the webfinger step and packing it all into the query step? (I've sorta proposed this before, but can't remember what the conclusion was.)

GET /.well-known/spsp.json?sender_account=example.sender&sender_kyc_ HTTP/1.1

If you allow redirects, is there any reason this wouldn't work?

Gets rid of another round-trip in the case where there is no redirect. And do we lose anything important?

Propose the following to be a bit more REST'y:

GET /api/receiver/alice/spsp HTTP/1.1

Propose that we add invoice as a sub-resource and a query string param to solve for the invoice use case better:

  • invoice_id - if specified then include the invoice

Note that it's also possible to allow any amount to be paid against an invoice. Let's not specify that in the protocol. The receiver can decide if they want the whole invoice paid (max and min are the same) or some minimum amount.

Also propose we don't try to specify anything more than the minimum required for display in SPSP. i.e. Let's not define the different statuses that an invoice can be in and rather just leave this as a handy way to link to other protocols that deal with this stuff like UBL and PEPPOL etc?

GET /api/receiver/alice/spsp?invoice_id=923169e6-7ee8-417e-9908-4453bab61c69 HTTP/1.1
Host: red.ilpdemo.org
Accept: application/json

{
  "shared_secret": "ZiI6-BbXMNud44kz2zIjlS_mztonBC0AKbeQqLmsVik",
  "destination_account": "example.main.alice.invoices.3463266",
  "maximum_destination_amount": "15.76",
  "minimum_destination_amount": "0.01",
  "receiver": {
    "name": "Alice",
    "image_url": "https://example.com/api/receivers/alice/profile_pic.jpg"
  }, 
  "invoice": {
    "id": "923169e6-7ee8-417e-9908-4453bab61c69",
    "subject": "Invoice for order number #346266",
    "href": "https://example.com/invoice_system/invoices/346266",
  }, 
  "ledger": {
    "currency_code": "USD",
    "currency_symbol": "$",
    "amount_scale": "4",
    "amount_precision": "10"
  }
}

@justmoon: I'd consider a new .well-known URL just another way to get the same data so we could think of it as an enhancement of SPSP. If I get an account@host payee identifier then I can try the .well-known/spsp endpoint but if I don't have any success then I revert to WebFinger and then SPSP. Or, are you proposing that we don't bother with WebFinger at all?

Another thought, to build on @justmoon's sender_account proposal.
All invoices for a sender.

GET /api/receiver/alice/spsp?sender_account=example.other.bob HTTP/1.1
Host: red.ilpdemo.org
Accept: application/json

{
  "shared_secret": "ZiI6-BbXMNud44kz2zIjlS_mztonBC0AKbeQqLmsVik",
  "destination_account": "example.main.alice.invoices.346266-346267",
  "maximum_destination_amount": "15.76",
  "minimum_destination_amount": "0.01",
  "receiver": {
    "name": "Alice",
    "image_url": "https://example.com/api/receivers/alice/profile_pic.jpg"
  }, 
  "invoices": [{
    "id": "923169e6-7ee8-417e-9908-4453bab61c69",
    "subject": "Invoice for order number #346266",
    "href": "https://example.com/invoice_system/invoices/346266",
  },
  {
    "id": "uh3469e6-7ee8-417e-9908-4453bab61c69",
    "subject": "Invoice for order number #346267",
    "href": "https://example.com/invoice_system/invoices/346267",
  }], 
  "ledger": {
    "currency_code": "USD",
    "currency_symbol": "$",
    "amount_scale": "4",
    "amount_precision": "10"
  }
}

Continuing the discussion on the issue thread, because then people get notifications about new activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment