Skip to content

doawoo/macaroon

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

53 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Macaroons (For Elixir)

Elixir CI Coverage Status MIT License Hex.pm

Cookies but better. For Elixir.

Fully Functional But Probably Needs More Testing :)

Requires: libsodium (can usually be easily installed using your favorite package manager)


Table of Contents


If you'd like to know all the details about Macaroons, I encourage you to read the research paper!

I'll summarize it up a bit below:

What are they?

Basic Summary

Macaroons are bearer credentials, similar to cookies, API tokens, or JWTs. They're presented upon each of a client's request. Where Macaroons differ from most bearer credentials are the fact that they securely embed caveats (permissions, reasons, capabilities, etc.) inside the credential itself. These caveats are signed using a secret key, so the target service can trust the credential as it is presented along with the client's request. The target service can evaluate the request, and the caveats to see if the operation is allowed.

Caveats

Caveats are simple statements that define what capabilities, identities, or authority the Macaroon holds.

Here's an example list of caveats a Macaroon may hold pertaining to an imaginary file sharing service:

1. user_id = 1234
2. user_upload_limit = 4MB
3. user_download_limit = 100MB
4. upload_namespace=/users/1234/*
5. timestamp <= 1/10/2021-5:48:47PM

With the examples above, the service should respect the requested operation should it meet the Macaroon's declared and signed caveats.

These caveats can contain any information in any string-based format. It's up to the service author to design the predicate language used.

Verification

When operating a service, you can verify a Macaroon "exactly" or "generally".

Exact verification means the data of the caveat must match byte-per-byte.

General verification allows the service author to provide simple callbacks which receive the caveat and can return true or false to indicate if it is met.

Discharging

When you want to have a third-party validate a caveat, you must have them issue you a "discharge" Macaroon that can prove that specific caveat. There are 2 well know ways to do this:

Well-Known RSA public key

(this is my favorite method of third-party proof!)

  1. Establish a relationship between the two servers (in this case a public/private RSA key pair)
  2. Encrypt your third-party predicate using the add_rsa_third_party_caveat/5 function
  3. Send this Macaroon to the client -- which will read the location and send the caveat id to the third-party server
  4. The third-party server will use the decrypt_rsa_third_party_caveat/3 function to take apart the cipher text into the predicate and the root key
  5. The third-party server will create a discharge Macaroon using the root key extracted from the cipher text in step 4 -- bind it to the original Macaroon
  6. Client will receive the new discharge Macaroon, and send that AND the original Macaroon back to the first-party service for verification

Round Trip

  1. Generate a nonce, then make some form of remote call out to the third-party service informing it of that random nonce
  2. The third-party service should return a unique ID, use this unique ID as the caveat ID in the third-party caveat. associate the unique ID with the random nonce that was generated
  3. Send this Macaroon to the client -- which will read the location and send the caveat id to the third-party server
  4. The third-party server will use the nonce to look up what needs to be verified
  5. The third-party server will create a discharge Macaroon using the nonce you sent it as the root key -- bind it to the original Macaroon
  6. Client will receive the new discharge Macaroon, and send that AND the original Macaroon back to the first-party service for verification

Examples

Creating a Macaroon

m = Macaroon.create_macaroon("http://my.cool.app", "public_id", "SUPER_SECRET_KEY_DO_NOT_SHARE")

Adding Caveats

m = Macaroon.create_macaroon("http://my.cool.app", "public_id", "SUPER_SECRET_KEY_DO_NOT_SHARE")
  |> Macaroon.add_first_party_caveat("upload_limit = 4MB")
  |> Macaroon.add_first_party_caveat("upload_namespace = /users/1234/*")
  |> Macaroon.add_third_party_caveat("https://auth.another.app", "PREDICATE_HOPEFULLY_ENCRYPTED", "RANDOM_SECRET_NONCE_KEY")

Verification

alias Macaroon.Verification

result = Verification.satisfy_exact("upload_limit = 4MB")
  |> Verification.satisfy_exact("upload_namespace = /users/1234/*")
  |> Verification.satisfy_exact("time < 2022-01-01T00:00")
  |> Verification.verify(macaroon, "SUPER_SECRET_KEY_DO_NOT_SHARE")

# result will be {:ok, macaroon} or {:error, reason_for_failure}

Serialization and Deserialize

JSON

{:ok, json_string} = Macaroon.create_macaroon("http://my.cool.app", "public_id", "SUPER_SECRET_KEY")
  |> Macaroon.serialize(:json)

macaroon = Macaroon.deserialize(json_string, :json)

Binary

{:ok, url_base64_string} = Macaroon.create_macaroon("http://my.cool.app", "public_id", "SUPER_SECRET_KEY")
  |> Macaroon.serialize(:binary)

macaroon = Macaroon.deserialize(url_base64_string, :binary)

Misc

Building on Apple Silicon

While the enacl dependency is awaiting some PRs to fix the build flags on Apple Silicon machines, you can work around this easily:

BEFORE you run mix deps.compile do the following

  1. Install libsodium via Homebrew: brew install libsodium
  2. Export the environment variables so Clang can find the library:
export CPATH=/opt/homebrew/include
export LIBRARY_PATH=/opt/homebrew/lib
  1. Export some extra C, C++ and Linker flags to build a dual-arch library (instead of just an x86_64 one):
export CFLAGS="-arch arm64"
export CXXFLAGS="-arch arm64"
export LDFLAGS="-arch arm64"
  1. Done! Now run mix deps.compile

Building on Windows

(I really recommend using the Windows Linux Subsystem. It makes installing libsodium and most other things much easier. But if you must run this natively on Windows, follow these tips!)

  1. Download the latest release of libsodium, compile it using Visual Studio's compiler using x86 ReleaseDLL config.
  2. Take note of the full path where the .dll, .lib are generated. Also note where the include directory is located.
  3. Rename the generated .lib to .dll.a.

Then using a Developer Command Prompt navigate to your project:

  1. set lib=%lib%;<PATH_TO_FOLDER_THAT_CONTAINS_libsodium.dll.a>
  2. set include=%include%;<PATH_TO_FOLDER_THAT_CONTAINS_sodium.h>
  3. mix deps.get and mix deps.compile

🍪 Baked with 🐾 by Digit (@doawoo) | https://puppy.surf