Auto-Peering

Based on some discussion with @sappenin on ILP over HTTP and auth between peers and some related discussions on “auto-discovery” here’s a high level proposal for an auto-peering protocol.

Assumptions

  • Two peers that support ILP over HTTP
  • Both peers are running HTTP servers on the public internet for this purpose and the path (relative to their connector’s base URL) for sending ILP packets is standardised to /ilp.

Goal

Alice wishes to peer with Bob who accepts unsolicited connections (an ILSP). Bob advertises his connector’s address as https://bob.example.

Flow

Alice sets up her connector at https://alice.example and configures it to peer with Bob at https://bob.example.

  1. Alice’s connector makes a POST request to https://bob.example/register with the payload:
{
  "name": "alice",
  "relation": "child",
  "assetScale": 2,
  "assetCode": "USD",
  "url": "https://alice.example",
  "auth": ["jwt-hmac256"]
}
  • name is optional and is just a friendly name alice wishes to use (she is identified by her URL). The connector might use it as an address suffix.
  • relation is optional and defaults to child
  1. Bob sets Alice up as a peer on his system and responds with:
{
  "jwt-hmac256": "uywdf66rFGFd8u==" //Secret to use to sign JWT
}
  1. Alice sends her first packet to https://bob.example/ilp
    The request contains an Authorization: Bearer XXXX header where XXXX is a JWT created by Alice.
    The JWT uses the HMAC256 signing algorithm and has at least one claim, the subject (sub) which is equal to her connector’s base URL (https://alice.example).
    The first packet will likely be an IL-DCP request to get her address.

  2. Alice sends a subsequent packet to peer.settlement.config to setup settlement for her account (details still being figured out but discussion started here)

  3. Alice receives payments from Bob when Bob sends packets to Alice. These are sent by Bob to https://alice.example/ilp also using JWT bearer tokens. In this case the subject of the token is https://bob.example.

I really like this proposal, @adrianhopebailie – nice work!

Three comments/suggestions:

First, I suggest we align more overtly support the iss, sub, and aud claims defined in RFC-7519.

  • iss: (issuer) claim identifies the principal that issued the JWT. ==> Alice or Bob’s server, or some 3rd-party that both trust
  • aud: (audience) claim identifies the recipient that the JWT is intended for. ==> Bob’s server if Alice is generating the token. Allows Alice to generate a more narrowly-scoped token, which improves security.
  • sub: (subject) claim identifies the principal that is the subject of the JWT.

Second, I suggest we allow the Connector processing the registration (https://bob.example in your example) to respond with its decision for what Alice should use as the sub claim when making requests to Bob. The primary rationale is that Bob is the one tracking this registration, and if he decides to identify alice by some other identifier that’s not her URL, then we should easily allow for that.

Third, we should encourage token creators to limit the expiry of any created tokens using the exp claim.

So, to those ends, your example response from Bob would instead look like this:

{
  "subject": "https://alice.example",
  "jwt-hmac256": "somesecret"
}

but it might, at Bob’s discretion, look like this:

{
  "subject": "alice-e7e640f6-0675-43bb-b02a-bced1f1a816b",
  "jwt-hmac256": "somesecret"
}

Finally, when Alice constructs a token to present to https://bob.example, it would look like this:

{
  "iss": "https://alice.example",
  "aud": "https://bob.example",
  "sub": "alice-e7e640f6-0675-43bb-b02a-bced1f1a816b",
  "iat": 1516239022,
  "exp": 1516239322
}

You can paste this token into jwt.io to see more details:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FsaWNlLmV4YW1wbGUiLCJhdWQiOiJodHRwczovL2JvYi5leGFtcGxlIiwic3ViIjoiYWxpY2UtZTdlNjQwZjYtMDY3NS00M2JiLWIwMmEtYmNlZDFmMWE4MTZiIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyMzkzMjJ9.z72acKvdHfcyRKcseTXsBU9fdZ9UF2aCRxUoed5CAD0
1 Like

Agree that we should be more explicit about the claims to use. I want to refine the response format a bit too.

What I like about using the URL is we don’t need another round trip for Bob to get a ‘sub’ value back from Alice for him to use in his token

Yeah, good points. For clarity, I’m suggesting we keep the url property in the request to Bob’s .../register endpoint (like you have it), but we would add one more field so that Alice can identify which sub Bob should use.

It would loosely be something like this, but the payload-naming is confusing.

{
  "name": "alice",
  "relation": "child",
  "assetScale": 2,
  "assetCode": "USD",
  "????": "https://bob.example",
  "url": "https://alice.example",
  "auth": ["jwt-hmac256"]
}

When Alice makes a registration request to Bob, what do we call the “details Bob will make calls to Alice with”? This doesn’t have an expressly defined name in the ILP-over-HTTP spec.

Perhaps we should give a name to each side of an ILP-over-HTTP connection, like “Incoming Link” and “Outgoing Link”, where in this case we’re using those terms from Alice’s perspective.

The registration payload would conceptually look like this:

{
  "name": "alice",
  "relation": "child",
  "assetScale": 2,
  "assetCode": "USD",
  "incomingIssuer": "https://bob.example", // Bob issues tokens for his own use.
  "incomingSubject": "bob-usd-account", // Bob's identify at Alice
  "incomingAudience": "https://alice.example", // Token is limited to usage at Alice
  "incomingAuth": ["jwt-hmac256"] // Alice will use this auth method to verify Bob's tokens
  "outgoingIssuer": "https://alice.example", // Alice's issues tokens for her own use.
  "outgoingSubject": "alice-usd-account", // Alice's identity at Bob
  "outgoingAudience": "https://bob.example", // Token is limited to usage at Bob
  "outgoingAuth": ["jwt-hmac256"] // Bob will use this auth method to verify Alice's tokens
}

Not really happy with any of these payloads, but want to be sure we don’t artificially limit the API endpoint. Ideally, we allow for all the incoming/outgoing variations, but maybe have some sensible defaults if a caller doesn’t want to specify everything.

I don’t think the registration API should be specific to ILP over HTTP. Why not make it so we can use this for BTP as well or other bilateral protocols we might want to add in the future?

Isn’t it considered bad practice to make assumptions about the locations of specific resources? I think we should say that you get some registration URL (that may or may not be the base URL + /register), POST/PUT a specific request to it, and then the response would include the details you should use for ILP over HTTP and other communication methods. Then we don’t need to be prescriptive about the node’s API layout (for example, what if we end up with multiple versions of the API and I want to put the first version at base URL + /v1/...?).

Is this assumed to be unique for that node? What should happen if there’s already another account with that name?

The party sending the request should send the specific details of how this node should communicate with them, rather than making the assumption about the path they should talk to.

What’s the rationale for forcing everyone to use JWTs? This use of them seems quite a bit more complicated than having each party generate a token for the other one to use to authenticate with them and I’m not sure it really provides much better security (unless we’re also going to design things like a key rotation method, etc).

If peers want more secure / complex authentication methods, it seems like we can just say those are manually configured. For the open signup, the relationships may not be that long-lived and I think we could have a simpler auth mechanism.

1 Like

I don’t think the intention is to force everyone to use JWTs (though there are plenty of security reasons to use them instead if passing a simple shared secret in each request). The auth-type array above could easily support other types, like a simple shared secret. Java and JS implementations of ILP-over-HTTP currently support both simple shared secret and JWT already, so this protocol leaves room for that.

If we’re saying open-reg is for shorter- lived relationships then shared secret should be fine. But that’s not sufficient for any long-term peering auth (IMHO).

That’s not my intention, but I wanted to have some “sane defaults” that allows us to spec something simple but extensible.

A future update might allow the requestor so specify alternative bilateral protocols but for now this “none specified” = “ILP over HTTP”.

I don’t think so. If we define an API (using Swagger or Open API or similar) then it is a set of resource paths relative to some base.

An alternative could be to define a standard response to a GET at the base that provides those details? See revised proposal below.

I don’t think JWT Bearer is hard. It’s already implemented in the JS and Java ILP over HTTP implementations, and it’s solving a lot of other issues with auth (like identifying the subject) as @sappenin points out.

Simple shared secret is a bad auth protocol IMO because it makes assumptions about the competency of the participants to manage lifecycle etc.

I’d prefer to say JWT’s are the default standard but leave it open for people to specify others (ideally from the list of widely used schemes in the Open API 3.0 spec not "roll-your-own"s.

Assets

Based on discussion so far I’d also propose we drop any data exchange around assets and assume that if you are auto-peering you are going to be the child and the server will be the parent.

Let’s keep this focused on:

  • discovering the bilateral protocols the server supports
  • requesting to peer with the server
  • getting authentication credentials to use with one of the available bilateral protocols
  • creating the bilateral connection

Once this is complete the peers can exchange ILP packets to setup settlement (and by implication determine the underlying asset).

This does raise some questions:

  • What if you peer with someone, only to discover they don’t offer settlement in the asset or using the settlement protocol you want?
  • Is there a benefit to the server, when it issues you credentials, in knowing what asset you plan to transact in?

Proposal 2

Alice sets up her connector at https://alice.example and configures it to peer with Bob at https://bob.example .

  1. Alice’s connector makes a GET request to https://bob.example and get’s back an Open API 3 specification in JSON format, something like:
{
    "openapi": "3.0.2",
    "info": {
        "version": "1.0.0"
    },
    "paths": {
        "/register": {
            "post": {
                "operationId": "registerPeer",
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/Peer"
                            }
                        }
                    }
                }
            }
        },
        "/ilp": {
            "post": {
                "operationId": "receiveIlpPrepare",
                "security": [
                    {
                        "jwt-bearer-token": []
                    }
                ]
            }
        }
    },
    "components": {
        "securitySchemes": {
            "jwt-bearer-token": {
                "type": "http",
                "scheme": "bearer",
                "bearerFormat": "jwt"
            },
           "api-key": {
               "type": "apiKey",
               "in": "header",
               "name": "X-API-KEY" 
           }
        }
    }
}

This is a standard Open API v3 schema, trimmed down to only provide the paths and security schemes. If we standardise on the operationId then you can map it to whatever path you want.

Pros: This is a well-supported standard for defining APIs and provides a lot of flexibility.
Cons: Seems way over complicated for our needs. (We could define this API using Open API v3 once and just get all servers to implement it at using a standard base.) It also provides no info on settlement options.

Let’s imagine a simpler return value from the GET request to the base URL:

{ 
  "ilp": {
    "http": {
      "version": "1.0",
      "url": "https://bob.example/ilp-service", // Default to /ilp but allow for custom
      "security": [ "jwt-bearer-token" ]
    },
   "btp": {
      "version": "1.0",
      "url": "wss://bob.example/btp",
      "security": [ "jwt-bearer-token" ]
   }
  },
  "settlement": {
    "XRP": [ "xrp-paychan", "xrp-ledger"],
    "BTC": ["lightning"]
  }
}
  1. Alice’s connector makes a POST request to https://bob.example/register with the payload:
{
  "ilp": {
    "http": {
      "version": "1.0",
      "url": "https://alice.example/ilp",
      "security": {
        "jwt-bearer-token": {
          "iss": ["*"], //List of issuers Alice will allow. "*" for any
          "sub": "bob" //Subject Bob must use
          "aud": "https://alice.example"
        }
      }
    },   
  },
  "settlement": {
    "XRP": ["xrp-paychan"] // Alice will settle using XRP Paychan
  }
}
  1. Bob sets Alice up as a child peer on his system and responds with:
{
  "ilp": {
    "http": {
      "version": "1.0",
      "url": "https://alice.example/ilp",
      "security": {
        "jwt-bearer-token": {
          "secret": "uywdf66rFGFd8u==" //Secret to use to sign JWT
          "iss": ["https:/alice.example"], //List of issuers Bob will allow.
          "sub": "alice-xrp" //Subject Alice must use
          "aud": "https://bob.example"
        }
      }
    },   
  }
  1. Alice sends her first packet to https://bob.example/ilp
    The request contains an Authorization: Bearer XXXX header where XXXX is a JWT created by Alice.
    The JWT uses the HMAC256 signing algorithm and has at least the following claims:
  • the subject ( sub ): alice-xrp
  • the issuer (iss): https://alice.example
  • the audience (aud): https://bob.example

The first packet will likely be a peer.settle.config request to get her settlement arrangement setup (details still being figured out but discussion started here )

  1. After getting a settlement configured Alice sends an IL-DCP request to get her address and find out what scale she must use in ILP packets.

  2. Alice receives payments from Bob when Bob sends packets to Alice. These are sent by Bob to https://alice.example/ilp also using JWT bearer tokens. In this case the claims in the the token are:

  • the subject ( sub ): bob
  • the issuer (iss): https://bob.example
  • the audience (aud): https://alice.example