Project

kransekake

0.0
No release in over 3 years
A pure Ruby implementation of macaroons for flexible authorization credentials
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

Kransekake

A pure Ruby macaroons implementation. Interoperable with go-macaroon v2.

Named after the Norwegian ring cake, which is a type of macaroon.

Macaroons are bearer tokens that support attenuation (anyone can add restrictions, no one can remove them) and third-party delegation, making them well-suited for decentralized authorization.

Installation

gem install kransekake

Or add to your Gemfile:

gem "kransekake"

For third-party caveats, also add rbnacl and install libsodium:

gem "rbnacl", "~> 7.0"

First-party-only usage works without either.

How it works

Macaroons use HMAC-SHA256 chaining. The root signature is derived from a secret key and each caveat extends the chain — caveats can be added but never removed, and tampering breaks the signature.

Instances are immutable Data objects. add_caveat returns a new macaroon, leaving the original unchanged.

base = Kransekake.new(key: "secret", identifier: "we used our secret key")
restricted = base.add_caveat("account = 3735928559")

base.caveats.empty?       # => true
restricted.caveats.size   # => 1

Usage

Minting and attenuating

Mint a macaroon with a secret key. Anyone can add caveats but never remove them.

require "kransekake"

key = "this is our super secret key; only we should know it"

base = Kransekake.new(key:, identifier: "we used our secret key", location: "http://mybank/")

macaroon = base
  .add_caveat("account = 3735928559")
  .add_caveat("action = deposit")
  .add_caveat("time < 2035-01-01T00:00")

Verifying

Every caveat must be satisfied. Pass a string for exact match or a block for a general predicate.

verifier = Kransekake::Verifier.new
verifier
  .satisfy("account = 3735928559")
  .satisfy("action = deposit")
  .satisfy { |caveat| caveat.start_with?("time < ") }

verifier.verify(macaroon:, key:) # => true

A wrong key, unsatisfied caveat, missing discharge or tampered signature raises a Kransekake::VerificationError subclass.

Serialization

Binary (v2 format), base64 and JSON.

binary = macaroon.serialize
Kransekake.deserialize(binary)

base64 = macaroon.serialize(base64: true)
Kransekake.deserialize(base64, base64: true)

json = macaroon.to_json
Kransekake.from_json(json)

Third-party caveats

Delegate verification to an external service by adding a caveat only the third party can discharge. Third-party caveats use NaCl secretbox (XSalsa20-Poly1305) to encrypt caveat keys. Requires rbnacl and libsodium.

root_key   = "this is our super secret key; only we should know it"
caveat_key = "this is a different super-secret key; never use the same secret twice"

# The authorizing service adds a third-party caveat
base = Kransekake.new(key: root_key, identifier: "we used our secret key", location: "http://mybank/")

macaroon = base
  .add_caveat("account = 3735928559")
  .add_third_party_caveat(key: caveat_key, identifier: "auth-session-id", location: "http://auth.mybank/")

# The third-party service mints a discharge macaroon
discharge = Kransekake.new(key: caveat_key, identifier: "auth-session-id", location: "http://auth.mybank/")
  .add_caveat("email = alice@example.org")

# Bind the discharge to the authorizing macaroon before sending
bound = macaroon.bind(discharge)

# Verify with both the macaroon and its discharge
verifier = Kransekake::Verifier.new
verifier.satisfy("account = 3735928559")
verifier.satisfy("email = alice@example.org")
verifier.verify(macaroon:, key: root_key, discharges: [bound]) # => true

Bundles

Package a macaroon with its bound discharges for transport. Supports binary, base64 and JSON.

bundle = macaroon.bundle(discharges: [bound])
restored = Kransekake::Bundle.deserialize(bundle.serialize)

verifier.verify(macaroon: restored.macaroon, key: root_key, discharges: restored.discharges)

Signature-only verification

Verify the HMAC chain without checking caveats. Returns first-party conditions.

macaroon = Kransekake.new(key: "secret", identifier: "we used our secret key")
  .add_caveat("account = 3735928559")

verifier = Kransekake::Verifier.new
conditions = verifier.verify_signature(macaroon:, key: "secret")
# => ["account = 3735928559"]

Trace

Step-by-step trace for debugging the HMAC chain.

macaroon = Kransekake.new(key: "secret", identifier: "we used our secret key")
  .add_caveat("account = 3735928559")

verifier = Kransekake::Verifier.new
trace = verifier.trace(macaroon:, key: "secret")

trace.ok?           # => true
trace.map(&:type)   # => [:derive_key, :hmac, :hmac, :verify_ok]

Each Kransekake::Trace::Op has type, input and output attributes.

Pattern matching

Data class, so deconstruction works:

macaroon = Kransekake.new(key: "secret", identifier: "user:alice@example.org")
  .add_caveat("account = 3735928559")

case macaroon
in identifier: /^user:/, caveats: {list: [Kransekake::Caveat => caveat]}
  puts caveat.identifier # => "account = 3735928559"
end

Choosing secrets

The examples above use human-readable keys for clarity. Random bytes are recommended in practice:

key = SecureRandom.random_bytes(32)

Interoperability

Interoperable with go-macaroon v2. Binary and JSON serialization, first-party verification, third-party caveat encryption (NaCl secretbox) and discharge binding all work bidirectionally. The test suite includes golden vectors from gopkg.in/macaroon.v2.

Requirements

  • Ruby 4.0+
  • rbnacl (~> 7.0) and libsodium for third-party caveats (optional)
    • macOS: brew install libsodium
    • Debian: apt install libsodium-dev
    • Arch: pacman -S libsodium