storj/docs/blueprints/access-revocation.md
Isaac Hess df0ef7e0cd docs: update access-revocation blueprint
This update includes the plan for authorizing macaroon revocation
requests.

Change-Id: I859fa7c23f3bc1c8a92741df6f88187547613303
2020-06-01 16:33:13 +00:00

6.1 KiB

Access revocation

Abstract

This blueprint describes a design to implement access revocation on the satellite.

Terminology

An access is a serialized data structure that contains the information necessary to authorize actions on the Storj network. An access contains a macaroon.

A macaroon is a structured token included in each call to the satellite. The macaroon contains a key identifier, any "caveats" that may restrict the macaroon's privileges, and a signature. (For more information about macaroons, please see https://research.google/pubs/pub41892).

A tail is a cryptographic hash of a portion of the macaroon, generated to validate the macaroon. In the process of macaroon validation, a chain of tails is generated. First the macaroon's root identifier is signed with a secret to generate the first tail. That tail is used as the "secret" to sign the next portion of the macaroon (a caveat) to produce a second tail. This process continues until the entire macaroon has been signed, which results in a final tail.

Background

Currently there is no way to revoke a macaroon once it is created. Because a macaroon is validated by a cryptographic signature, once signed it is always cryptographically valid. Thus, if the macaroon does not contain an expiration caveat its privileges will never be revoked or expire.

The holder of a macaroon can create new, further-caveated macaroons without coordinating with a centralized service or with the entity that provided them with the macaroon. This makes macaroons ideal for distributed authorization, but presents challenges when revoking a macaroon.

For example, if I hold the API key for Project A, I can create Macaroon A with a caveat that it can only read and write files within Bucket A. I can then share this macaroon with my own customer, Customer A. Customer A may then, if they wish, create Macaroon B which is futher caveated -- for example, restricted to read-only access in Bucket A -- and share Macaroon B with someone else. This can occur without my knowledge.

I may then want to revoke Macaroon A. By so doing, I would want Macaroon B to also be revoked, and any other macaroons created from Macaroon A.

Design

Revoking a macaroon

We will create a satellite endpoint that receives revocation requests. A request to this endpoint must include the macaroon to revoke. The request must also include a macaroon authorizing this request. To be authorized, the authorizing macaroon must be a parent of the macaroon to revoke. This means the final tail of the authorizing macaroon must match a tail in the macaroon to revoke. In this way we allow a holder of a macaroon to revoke any further-attenuated macaroons that are based on the one they hold.

The satellite will respond with a success or failure, whether the macaroon was successfully revoked.

Handling revocation requests

The satellite will maintain a database of revoked final tails. When it receives a macaroon to revoke, it will first validate the request, then calculate the final tail for the macaroon, and store that value in a revocation database.

Checking for revocation

When a request comes in to the satellite, in addition to checking the macaroon for cryptographic validity, the satellite will check if it contains any tails that have been revoked. By checking each tail of the macaroon it ensures that any "sub-macaroons" created from a revoked macaroon will also be revoked.

The satellite will calculate the final tail for the macaroon and check it against an expiring bounded LRU cache to see if the tail has been seen recently. If the tail is in the cache, this cache will give the result for whether it is "valid" or "revoked". If the final tail is not in the cache, it will query the database.

The database query will include all tails in the macaroon to see if any of them are revoked. The result of this query will be stored in the cache, with the final tail being noted as "valid" or "revoked," so that the database does not need to be consulted again for this macaroon (within some configurable expiring window).

Rationale

This approach was deemed best because it:

  • Adds very little latency to each request.
  • Creates very little load on the database.
  • Is backwards compatible, and allows us to revoke existing macaroons.
  • Allows us to revoke an entire "macaroon tree" while maintaining the distributive properies of macaroons.

Disadvantages to this approach:

  • A satellite adds an additional piece of critical information (revoked tails) that it must never lose.
  • Revocation is not immediate. If a revoked key has been used recently, it will still be valid until it expires from the whitelist cache. With proper tuning this shouldn't be a big issue.

Other approaches considered:

  • Revocation as a third-party caveat. In this approach, a "revocable" macaroon contains a third-party caveat that must be discharged when submitting the macaroon to the satellite. The client must include a "discharge macaroon" with the regular macaroon that proves the macaroon is not revoked.
    • Advantages: when receiving a request, a satellite only needs to cryptographically verify the macaroon without having to check caches or a db.
    • Disadvantages: it's more complicated, existing macaroons cannot be revoked, extra latency required to get discharge macaroon.

Implementation

Database

I created a proof-of-concept database with a single indexed column storing the tails. I placed 1,000,000 fake revoked keys in the database, and benchmarked various scenarios (small macaroons, large macaroons, valid macaroons, revoked macaroons), and the database performed well.

We used the following query:

select exists(select 1 from revoked where tail in ([tails]))

When testing a macaroon with 500 caveats (much larger than a real-life scenario), with a valid macaroon, it resulted in an index-only scan and took less than 6 ms.

Cache

The whitelist/blacklist cache will be implemented in a way similar to our current api keys cache. It will be bounded LRU cache and will store the known final tail and a boolean for whether it is valid. In this way the same cache can be used for both whitelist and blacklist.

Open issues

  • None