-
Notifications
You must be signed in to change notification settings - Fork 111
Improve ILP-over-HTTP Auth #531
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
Changes from all commits
ca92ca3
2b92df0
d327631
d3c735f
d5c55bb
24bca61
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| --- | ||
| title: ILP Over HTTP | ||
| draft: 2 | ||
| draft: 3 | ||
| --- | ||
|
|
||
| # ILP Over HTTP | ||
|
|
@@ -20,10 +20,6 @@ Each ILP Prepare packet is sent as the body of an HTTP request to the peer's ser | |
|
|
||
| This is a minimal protocol built on HTTP. HTTP/2 is HIGHLY RECOMMENDED for performance reasons, although HTTP/1.1 MAY also be used. Implementations SHOULD support HTTP version negotiation via Application Protocol Negotiation (ALPN). | ||
|
|
||
| ### Authentication | ||
|
|
||
| Peers MAY use any standard HTTP authentication mechanism to authenticate incoming requests. TLS Client Certificates are RECOMMENDED between peers for security and performance, though bearer tokens such as JSON Web Tokens (JWTs) or Macaroons MAY be used instead. Basic authentication (username and password) is NOT RECOMMENDED, because of the additional delay introduced by securely hashing the password. | ||
|
|
||
| ### Request | ||
|
|
||
| ```http | ||
|
|
@@ -56,3 +52,119 @@ All ILP Packets MUST be returned with the HTTP status code `200: OK`. | |
|
|
||
| An endpoint MAY return standard HTTP errors, including but not limited to: a malformed or unauthenticated request, rate limiting, or an unresponsive upstream service. Connectors SHOULD either retry the request, if applicable, or relay an ILP Reject packet back to the original sender with an appropriate [Final or Temporary error code](./0027-interledger-protocol-4/0027-interledger-protocol-4#error-codes). Server errors (status codes 500-599) SHOULD be translated into ILP Reject packets with `T00: Temporary Error` codes. | ||
|
|
||
| ### Authentication | ||
| When authenticating requests between Interledger nodes, it is important to choose an algorithm that maintains an appropriate balance between usability and security, while at the same time maintaining high-performance _and_ interoperability. | ||
|
|
||
| In order to find this balance, this document defines two Authentication profiles, each with various trade-offs that should be considered before use: | ||
|
|
||
| * `SIMPLE`: Allows two ILP nodes to utilize a previously agreed-upon shared-secret as a [Bearer token](https://tools.ietf.org/html/rfc6750) in all ILP-over-HTTP requests. Peers SHOULD consider this token to be opaque and SHOULD NOT derive any special meaning from the token. | ||
|
|
||
| * `JWT_HS_256`: Allows two ILP nodes to utilize a previously agreed-upon shared-secret in order to _derive_ a Bearer token that conforms to the JSON Web Token (JWT) specification. JWTs generated by this scheme can then be used as a Bearer in all ILP-over-HTTP requests. | ||
|
|
||
| Peers MAY use any standard HTTP authentication mechanism to authenticate incoming requests, MUST support `JWT_HS_256`. | ||
|
|
||
| #### `SIMPLE` Authentication Profile | ||
| This profile allows two ILP nodes to utilize a previously agreed-upon shared-secret that contains at least 32 bytes (256 bits) of randomly generated data, and is encoded using Base64. | ||
|
|
||
| Because tokens in this profile do not inherently contain information about the identity of the caller, requests MUST contain an additional HTTP request-header named `Auth-Principal`. | ||
|
sappenin marked this conversation as resolved.
|
||
|
|
||
| This extra header allows for the identity of the authentication request to be separated from authentication token itself, which reduces computational overhead as well as data-management complexity (e.g., implementations do not need to create data-store indexes using derivations of tokens for lookup purposes). | ||
|
|
||
| ##### Example Usage | ||
| An example shared-secret in this profile is `HEiMCp0FoAC903QHueY89gAWJHo/izaBnJU8/58rlSI=`. This shared secret is passed as an `Authorization` header in each HTTP request, using the Bearer token scheme, along with an `Auth-Principal` header like this: | ||
|
|
||
| ``` | ||
| Auth-Principal: alice-usd-123 | ||
| Authorization: Bearer HEiMCp0FoAC903QHueY89gAWJHo/izaBnJU8/58rlSI= | ||
| ``` | ||
|
|
||
| Implementations MAY support this profile, but SHOULD consider it for development purposes only. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think there's anything wrong with using a bearer token in production. If someone is OK with the pros/cons below then that's fine |
||
|
|
||
| ##### Trade-off Summary | ||
| * **Pros** | ||
| * The simplest, most usable Authentication profile -- just a shared-secret with _at least_ 32 bytes and an identity header. | ||
| * Very little processing time to verify a token (just a simple value comparison or, for more security, an HMAC-SHA256 computation). | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's very important that a constant-time compare is used for verifying this token; we should note that somewhere (maybe this point isn't the place for that although suggesting a 'simple value comparison' could lead people into the pitfall of using an ordinary string compare which is not constant time and leaks information about the secret even on an incorrect guess) |
||
|
|
||
| * **Cons** | ||
| * The shared-secret is transmitted "on the wire" for every request, increasing the chances that it might be intercepted by a compromised TLS session (e.g., a [MITM attack](https://en.wikipedia.org/wiki/Man-in-the-middle_attack)); a TLS termination endpoint (e.g., a Load Balancer); or logged by an internal system during transit. | ||
| * The shared-secret itself never expires, so if an implementation neglects to rotate the secret with its peer, this token will likely be very long-lived. This increases the chance of compromise by an attacker, and means compromised usage of this type of token could go undetected for very longer periods of time. | ||
| * Requires out-of-band communication for both peers to agree upon the shared secret. | ||
|
|
||
| #### `JWT_HS_256` Authentication Profile | ||
| This profile allows two ILP nodes to utilize a previously agreed-upon shared-secret, but then derive an [RFC-7519](https://tools.ietf.org/html/rfc7519) compliant JWT token in order to perform actual authentication. | ||
|
|
||
| ##### JWT Claims | ||
| In order to be considered a valid JWT for this profile, the signed JWT MUST contain a `sub` (subject) claim containing the identifier of the "principal" that the token authenticates. | ||
|
|
||
| A JWT token SHOULD also include an `exp` (expiry) claim that indicates a date/time after which the token should be considered invalid. Implementations SHOULD reject any tokens with a missing or invalid expiry claim. | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In JWT-land, there are two other very common claims that are typically included in a JWT, and they are probably worth talking about in the context of ILP-over-HTTP:
RFC-7519 clearly marks these two claims as optional, though it does define concrete behaviors if these claims are present in a JWT. For the purposes of this Authentication Profile in this RFC (i.e.,
I'm inclined to update this proposal to include the
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. technically if you try to present a JWT to someone other than the intended audience then they'll already reject it because you're gonna have a different shared secret with each of your peers |
||
|
|
||
| Note that tokens without this claim never expire, and only become invalid if the shared-secret used to sign the JWT changes or is otherwise invalidated. | ||
|
|
||
| ##### Example Usage | ||
| In this profile, the Base64-encoded JWT is passed as an `Authorization` header in each HTTP request, using the [Bearer token](https://tools.ietf.org/html/rfc6750) scheme. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: the encoding of a JWT already uses base64 segments delimited by periods so base64-encoding a JWT doesn't make sense |
||
|
|
||
| One example of such a Bearer token: | ||
|
|
||
| `Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbGljZSJ9._Jn0pkqrK1leE3WZJKn-g5hm5kGJxGdSHggtz5wO1w4`. | ||
|
|
||
| Using the JWT specification, this token can be verified using the shared-secret previously agreed upon. For example, the above token contains a `sub` claim of `alice` and can be verified using a shared-secret value of `HEiMCp0FoAC903QHueY89gAWJHo` (Base64 encoded). | ||
|
|
||
| Another example is a Bearer token that contains an expiration date: | ||
|
|
||
| `Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbGljZSIsImV4cCI6MTU1ODAzNTg2OH0.__9CiSGdn4Grhl48slun7Lp4q4xt0uq398omcipBU8M`. | ||
|
|
||
| Using the JWT specification, this token can be verified using the shared-secret previously agreed upon. For example, the above token contains a `sub` claim of `alice` and an `exp` claim of `1558035868`, which means this token is no longer valid after `May 16th, 2019 at 9:29:11 GMT`. This token can be verified using a shared-secret value of `HEiMCp0FoAC903QHueY89gAWJHo` (Base64 encoded). | ||
|
|
||
| ##### Trade-off Summary | ||
| * **Pros** | ||
| * Same usability as the `SIMPLE` profile -- just a shared-secret with _at least_ 32 bytes and a `sub` claim. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would drop this point because I don't think it really makes sense |
||
| * Allows both identity and authentication claims to be contained in single Bearer token, which eliminates the need for a second `Auth-Principal` header. This should simplify token validation and ease account-lookup (no need to create an HMAC or encrypted data-store index because the principal can be trivially decoded from the token). | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. technically it does create an HMAC in order to verify this |
||
| * Requires only enough processing to perform an HMAC-SHA256 signing operation, which is very fast. | ||
| * Supports token expiry, which allows tokens to be generally short-lived so that peers can narrow the potential window of unauthorized usage in the event of token compromise. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this only applies to a token compromise that occurs over the wire (MITM). If there's a compromise in the configuration or in the process itself then the attacker can create packets until the shared secret is rotated |
||
| * The actual shared-secret is _never_ transmitted "on the wire" during any request. Instead, authentication tokens are always _derived_ from the shared-secret, which eliminates the risk of an _actual_ shared-secret being intercepted in transit. | ||
|
|
||
| * **Cons** | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would also list complexity as a con, plus the fact that you need to perform several SHA256 computations and JSON serializations/deserializations on the sender and the receiver sides on each packet (can be kinda optimized if an identical JWT is used several times in a row though) |
||
| * Total transmitted bytes for authentication are more than the `SIMPLE` scheme (about 41 bytes, or ~50% more). However, HTTP/2 header compression should mitigate this differential. | ||
| * Requires out-of-band communication for both peers to agree upon the shared secret. | ||
|
|
||
| ## Appendix1: Security Best Practices | ||
|
sappenin marked this conversation as resolved.
|
||
| This section outlines and clarifies some best practices for authentication-token security when using this protocol. Recommendations in this section are _Non-Normative_, but highly RECOMMENDED. | ||
|
|
||
| ### Follow Standardized Security Recommendations | ||
| It is advisable to follow all applicable best practices when using a Bearer-token scheme for authentication. [Section 5.1 of RFC-6570](https://tools.ietf.org/html/rfc6750#section-5) contains a number of very good practices that should be considered on a per-deployment basis. | ||
|
|
||
| ### HTTP BASIC/FORM Auth | ||
| HTTP Auth schemes using a username and password is NOT RECOMMENDED for the same reasons that the `SIMPLE` profile is only recommended for development and testing scenarios only. | ||
|
|
||
| ### Use SIMPLE Profile for Development/Testing Only | ||
| The `SIMPLE` authentication profile provides only marginal benefits when compared to the `JWT_HS_256` profile, but introduces significant drawbacks as outlined in the "Trade-off Summary" sections of this RFC. As such, the `SIMPLE` profile MAY be used for development or testing purposes, but SHOULD NOT be used in production scenarios. Instead, prefer `JWT_HS_256` for production deployments. | ||
|
|
||
| ### Use Reasonable Token Expiries | ||
| Tokens generators should choose a reasonable token expiry. Considerations in this choice include the ability to cache and re-use authentication tokens for a limited time in order to enable very fast authentication decisions. However, this should be balanced against a desire for shorter token lifetimes, which will limit the attack surface caused by a compromised token. | ||
|
|
||
| As a best practice, implementations SHOULD use tokens that expire. For example, consider generating tokens with a lifetime that doesn't exceed 5 minutes. | ||
|
|
||
| ### Secrets At Rest | ||
| In most ILP Connector implementations, secrets are stored on a per-account basis in some sort of data-store. Implementations SHOULD protect secret-values that can be used to generate authentication tokens by encrypting them prior to storage. This will help prevent actual shared secrets from being captured by unauthorized parties, increasing the chances that only the Connector runtime will be able to generate tokens using underlying shared-secrets. | ||
|
|
||
| One option is to encrypt any secrets at-rest using an Authenticated Encryption algorithm, such as AES-GCM-256. This [article](https://proandroiddev.com/security-best-practices-symmetric-encryption-with-aes-in-java-7616beaaade9) provides a nice overview using Java, along with various configuration options to consider. | ||
|
|
||
| ### Secrets In Memory | ||
| Implementations SHOULD minimize the amount of time that an actual secret-value exists in-memory in unencrypted form. This includes narrowing the availability of secrets to only code that actually requires them; minimizing the time any secret might exist in memory; and zeroing out memory after a secret is no longer used, if possible. | ||
|
|
||
| ### Mutual TLS | ||
| All ILP-over-HTTP connections MUST be performed over a TLS session. However, it is also RECOMMENDED to use TLS Client Certificates between peers for additional security. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mutual TLS authentication is the industry standard for online payment APIs, particularly between things like banks and financial services companies. If you're using mutual TLS, you've already established the peer identity, and there's no need to use a secondary mechanism (e.g. JWT claims) to do as such. Unlike bearer credentials such as JWTs, X.509 peer identity is bound to the secure channel. This has pros and cons (mutual TLS makes authentication through a proxy difficult, where tokens generally carry higher exfiltration risk, and JWTs in particular risk immature/deficient implementations of a standard with a poorly thought out, overcomplicated cryptographic design), but I think the most important is that determining the validity of the remote peer can be handled directly by the TLS stack / application framework, as opposed to writing some ad hoc introspection logic for JWTs. I think X.509 + token based solutions can be helpful, but much less so when the token is claims-bearing (which in my book means an AuthN credential). A much better use of tokens-over-mutual-TLS, IMO, is using tokens for fine-grained AuthZ and floating AuthZ policies. If you'd like to explore this, my recommendation would be to start by keeping the wire format of the tokens opaque, but stipulating a standard authorization workflow (though I dislike it for several reasons, OAuth2 fits the bill). tl;dr: I would like to see this section expanded to include ways of obtaining identity information from the X.509 certificate presented by the peer during the TLS handshake. In particular I think it'd be interesting to explore ways of using Subject Alternative Names (SANs) in conjunction with things like ILP addresses.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hey @tarcieri, thanks for your input here!
In theory, this is true, and two years ago I would have agreed with you -- but in practice I've found that "there's no need to use a secondary mechanism" to be incorrect. The primary reason is that many entities (you can use "banks" here, but it's broader than just banks) terminate TLS connections at the network edge, and then proxy those connections (sometimes over HTTP, believe it or not) to the actual backend. In these deployments (where TLS is terminated), the statement that "X.509 peer identity is bound to the secure channel" is no longer true. The concept of authenticated identity has now been shifted to be: "Whoever the edge device says you are", which IMHO is a problem.
In my experience, because of the way entities outside of the control of software developers deploy their TLS connections, there must be a higher-level authentication scheme that exists outside of TLS.
Setting aside authorization for a moment, I think your suggestion about "keeping the wire format of the tokens opaque" is the opinion I held about a month ago (i.e., keeping the tokens opaque), but after trying to build something, I ran into the trade-offs outlined in the PR (see here) when using a I'm open to more feedback/ideas here though. The only reason I arrived at the
I've explored similar ideas for other payment networks, and think this could be a very interesting option for a part of an ILP identity layer. The questions are many though - I've started a forum thread here to discuss. Would love to hear more thoughts from you and others on that one. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I noted this to a certain extent earlier when I enumerated pros and cons. However, I'll also note that with a token-based solution, compromise of an edge terminator has an analogous but slightly less severe problem in that attackers can replay observed tokens (unless they are sufficiently time limited). This is the fundamental risk of edge termination. I'll quickly note an alternative to edge termination is passive SNI inspection. All that said: the information being provided is peer identity, a.k.a. AuthN/"claims". To avoid compromise of the edge terminator becoming a catastrophe, as I mentioned earlier supplemental AuthZ credentials can also be required. The advantage of relying on mutual TLS for identity and using tokens for AuthZ only is the token can be channel bound / "contextually confined" to a particular secure channel. This won't stop an attacker who pops the edge terminator, but it will stop one who exfiltrates a client-side token. The main disadvantage of relying on bearer credentials alone is exfiltration risk: there are many secure storage options for TLS private keys, including TPMs/TEEs/SEPs as well as HSMs and hardware tokens. It's significantly easier to keep them out-of-band of application logic and thereby prevent unintentional exposure. As written this suggests using mutual TLS, then ignoring the TLS peer information, in which case why are you using mutual TLS? Making the peer identity information encoded in the token a caveat/assertion (as in an AuthZ based scheme), rather than making the token the authority for it (as you're proposing), allows you to actually leverage the security provided by the MTLS channel. I'll quickly note there's RFC8471 Token Binding as a general purpose solution for binding tokens to a secure channel using an automatically provisioned self-signed certificate, however it's quite complex and not widely implemented (I believe Chrome has abandoned their efforts to implement it). I'll footnote this by saying you can find me arguing against client certificates in an unrelated conversation about API authentication for Rust's crates.io package repository. The difference there is the tokens are being used to identify human principals rather than services. But as I noted in that thread as well: "mutual TLS vs tokens" isn't a dichotomy. The two can be combined and leveraged as a force multiplier. I'd suggest you don't paint yourself into a corner and do attempt to leverage TLS peer identity info when available. |
||
|
|
||
| ### High Security Deployments | ||
| For deployments requiring very high security, it is recommended to utilize a secret-store deployed outside of the Connector runtime, such as [Vault](https://www.vaultproject.io/) or an [HSM](https://safenet.gemalto.com/data-encryption/hardware-security-modules-hsms/) or both. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we should have this recommendation until someone actually does this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd suggest making the wording here a bit more generic, e.g. "key management service" and "hardware key storage" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could also clarify whether the suggestion to use a key management service rather than storing plaintext secrets in the config, or if the connector should be requesting signed JWTs from some external service rather than generating them itself and therefore having access to the secret in memory |
||
|
|
||
| This will provide an extra layer of protection in case a Connector runtime is compromised, and will also make it significantly harder for an attacker to compromise actual shared-secret values (especially if employing an HSM). | ||
|
|
||
| However, before employing such a system, Connector operators SHOULD perform extensive performance testing to ensure proper levels of service. | ||
|
|
||
| ## Appendix2: Normative References | ||
| For more details on the algorithms and standards referenced in this RFC, see the following: | ||
|
|
||
| * RFC-6750: [Bearer Token Usage](https://tools.ietf.org/html/rfc6750) | ||
| * RFC-7518: [JSON Web Algorithms (JWA)](https://www.rfc-editor.org/rfc/rfc7518.html) | ||
| * RFC-7519: [JSON Web Token (JWT)](https://www.rfc-editor.org/rfc/rfc7519.html) | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This morning it occurred to me that since this PR proposal has added the
Auth-Principalheader, this profile is now exactly equivalent from a security perspective to HTTP BASIC Auth. Except that it uses different headers.This had me wondering if we shouldn't just replace the
SIMPLEAuth profile with HTTP Basic Auth so that implementations don't have to implement something custom here (My rationale is that nearly every HTTP client library in every language will support BASIC auth already).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After building an implementation of this PR in Java today, it counterintuitively turns out that I can re-use the
JWT_HS_256Authentication provider for both profiles (SIMPLEandJWT_HS_256).This is because I can implement it once for
JWT_HS_256, and then to supportSIMPLE, I simply give my peer anHS_256JWT with a very long expiry. From my peer's perspective, this just an opaque token, which it happily re-uses for a very long time.From that perspective, keeping
SIMPLEas a Bearer scheme actually reduces implementation work on the server. For the outgoing client though, it's a bit more of a pain, and using BASIC auth would be less work.All that to say, the more I think about it, I feel like even defining
SIMPLEis maybe a mistake. The PR is quite overt that nobody should use it in production, so I wonder if we just remove it entirely from the PR so that there's only one authentication profile that all implementations must normatively support.Any strong thoughts or opinions about keeping or getting rid of the
SIMPLEprofile?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy to get rid of it. It did occur to me on first reading it that a JWT with long expiry would probably be fine for testing and COULD be generated by the server anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm leaning towards removing
SIMPLE. I will wait a day or two in order to give others a chance to comment.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Simple is very useful for efficiency reasons. The replayability isn't such a big issue because HTTPS prevents replay attacks. It's mostly just a concern that if your credentials are compromised (which is an issue with a JWT secret anyways) they can forge packets and if the HTTPS connection is MITMed then the attacker could create new authenticated packets but that's not always a realistic attack vector