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 kransekakeOr 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 # => 1Usage
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:) # => trueA 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]) # => trueBundles
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"
endChoosing 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.