https://github.com/justenwalker/mack
A Go implementation of Macaroons - Cookies with Contextual Caveats
https://github.com/justenwalker/mack
authorization golang macaroons security
Last synced: 5 months ago
JSON representation
A Go implementation of Macaroons - Cookies with Contextual Caveats
- Host: GitHub
- URL: https://github.com/justenwalker/mack
- Owner: justenwalker
- Created: 2024-05-06T13:13:47.000Z (about 2 years ago)
- Default Branch: main
- Last Pushed: 2025-02-18T02:58:21.000Z (over 1 year ago)
- Last Synced: 2025-02-18T03:31:43.682Z (over 1 year ago)
- Topics: authorization, golang, macaroons, security
- Language: Go
- Homepage:
- Size: 84 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Mack - A Go library for interacting with Macaroons
## What is this library
Currently,
### It is:
1. Something I wrote up in my spare time to understand Macaroons a bit better.
2. A learning exercise for me to explore library design ideas.
3. A hobby/side project for me to obsess over.
### It is **not**:
1. A production-grade library you should bet your entire business and information security on.
2. An official or reference implementation of Macaroons.
3. Able to make Margaritas.
That said, I'd welcome some advice, comments, constructive feedback about the library, it's ergonomics, or rough edges. I'm also not a cryptographer or security researcher, so if there are any flaws in the implementation I'd like to know about them.
### Why?
The Macaroons paper specifies how to create this chained list of caveats, but doesn't have any opinions. Unlike JWT, there are really no standards what caveats are, or how to validate them. This library implements the base methods for
constructing and verifying macaroons and exposes interfaces for implementing the areas of the spec that are open to
interpretation, while also providing some opinionated implementations of these interfaces to make it actually useful.
### Full Example
An example implementation using this library is in the [example](./example) directory.
This covers constructing a Macaroon, discharging, validating, and clearing caveats.
## What is a Macaroon?
[See Paper](https://www.ndss-symposium.org/wp-content/uploads/2017/09/04_3_1.pdf)
A Macaroon is a security credential that support decentralized delegation.
It is implemented as a chain of signed "Caveats".
Macaroons are similar to [JWT](https://jwt.io) in that they are Bearer tokens that grant access to a resource. Where they differ is that, JWTs are minted by a token service with a fixed set of "**claims"** that cannot be altered; whereas as Macaroon may derive new sub-macaroons with more limited permissions, without coordinating with the service issuing the macaroon initially.
A **"caveat"** is different from a JWT claim. While a claim is some (authenticated) information about whom the bearer is, or what the bearer be able to do or what permissions they have, what groups they are from etc... a caveat only exists as a predicate on what the bearer is allowed to do: it limits the conditions under which the macaroon is valid.
For example, a list of claims may be:
```json
{
"user": "foo",
"groups": ["g1","g2"],
"permissions": ["p1","p2"]
}
```
Whereas a list of caveats might be:
- `request.path =~ "/user/.*"`
- `org in (org1,org2)`
- `app not in (sensitive_app)`
NOTE: some conventional JWT claims can be interpreted as caveats:
- `iss`: token was issuer by this issuer, to limit which token sources should be trusted.
- `aud`: token is valid for a specific audience, preventing token from `service-a` from being used on `service-b`
- `nbf`: token is not valid before this unix timestamp
- `exp`: token is no longer valid after this date
## Quick Tour
### Packages
- `mack` - The main package. These are where all the Macaroon primitive types and operations reside.
- `sensible` - Provides sensible default implementations of cryptographic functions.
- `thirdparty` - Provides a framework for constructing third-party caveats and discharging them.
- `thirdparty/exchange` - Implements interfaces in `thirdparty` by using encrypted caveat ids.
### Create a Macaroon Scheme
First, you must create a `mack.Scheme`.
You can create a new scheme with:
```go
scheme := mack.NewScheme(mack.SchemeConfig{
HMACScheme: hms,
EncryptionScheme: es,
BindForRequestScheme: b4rs,
})
```
Each configuration option is an interface implementing a specific cryptographic algorithm that will be part of the scheme.
- `HMACScheme` - Implements the algorithm is used to generate HMACs.
- `EncryptionScheme` - Implements functions used for Encryption/Decryption for Third-Party Caveats (HMAC Size and Encryption Key size must match!)
- `BindForRequestScheme` - Implements the function used to bind a discharge macaroon to an authorization macaroon.
### Sensible Defaults
There is a `sensible` package can be used for creating a `mack.Scheme` with sensible defaults:
- `HMACScheme`: HMAC-SHA256
- `EncryptionScheme`: AES-256-GCM with Random 96-bit Nonce
- `BindForRequestScheme`: discharge.Sig = `HMAC-SHA256(Auth.Sig, Discharge.Sig)`
### Create a Macaroon
New Macaroons can be constructed from the Scheme using the `NewMacaroon` function:
```go
// start with a scheme, in this case a sensible default
scheme := sensible.Scheme()
// Macaroon ID (nonce): Should be random, and never used again.
// - UUID might be a good choice.
id := make([]byte, 16)
rand.Read(id)
// Macaroon Root Key:
// You have to be able to associate the Macaroon ID with this key somehow.
// Perhaps you store the id/key pair in a DB or derive a key from a pre-shared password and the macaroon id.
key := make([]byte, scheme.KeySize())
rand.Read(key)
// Macaroon Caveats:
// Assemble the list of initial caveats.
// You should always have an initial set of at least 1 caveat on a macaroon.
// Without a caveat, a macaroon is permitted to do anything.
// Such a macaroon can be constructed with Scheme.UnsafeRootMacaroon, but this is not recommended.
caveats = [][]byte{
[]byte(`org = Organization`),
[]byte(`user = User`),
}
// Creates the initial macaroon.
m, err := scheme.NewMacaroon("https://www.example.com", id, key, caveats...)
```
### Add First-Party Caveats
First-party caveats are caveats that are cleared by the authorizing service.
The caveat ID is typically a predicate that describes a condition that must be true
if the macaroon is used. How it is parsed, and evaluated is up to the authorizing service,
so what the bytes represent is opaque to the macaroon scheme.
Additional caveats may be added to a macaroon using `Scheme.AddFirstPartyCaveat`
```go
m, err = scheme.AddFirstPartyCaveat(&m, []byte(`expires = 2006-01-02`))
```
### Validating Macaroon Stacks
An Authorizing Macaroon and its associated Discharge Macaroons constitute a `macaroon.Stack`.
Clients construct this stack by receiving discharge macaroons from third-parties for all of their third party caveats
and presenting both the authorizing macaroon and all discharge macaroons bound to it to the Authorizing Service.
Clients create a stack by using `Scheme.PrepareStack`.
```go
stack, err := scheme.PrepareStack(authorizingMacaroon, dischargeMacaroons)
```
The stack is then transmitted with the request. How the stack is encoded is implementation specific, so see [example](./example) for implementation.
After encoding the stack into bytes, it can be put into a request body or encoded as Base64 and added to an HTTP Authorization
header. Whatever the service expects.
The Service, after receiving this stack, should decode it.
Validation on the server side happens in two phases: Verifying, and Clearing.
Stack *Verification* only ensures that all the cryptographic signatures match their expected values,
but it doesn't say anything about the validity of the caveats on the macaroon.
```go
verifiedStack, err := scheme.Verify(ctx, key, stack)
```
The key in the above verification function is extracted from the Root Macaroon ID. As an example: this could be by looking it up
in a database, or by combining the id with some shared secret via an HMAC function to generate the key.
After getting a `VerifiedStack` from the `Scheme.Verify` function, the VerifiedStack should be cleared before
allowing the action:
```go
// checker is a PredicateChecker.
// A PredicateChecker interprets a caveat id, and evaluates its result.
// It returns true if the predicate is satisfied.
if err := verifiedStack.Clear(ctx, checker); err != nil {
// stack failed to clear
return err
}
// stack is verified, proceed with action
// ...
```
### Add Third-Party Caveats to a Macaroon (Attenuation)
While you can construct these values from scratch and use `scheme.AddThirdPartyCaveat`, the `thirdparty` package can help generate
and apply these values to a Macaroon via a `thirdparty.Attenuator`.
```go
tpa, err := thirdparty.NewAttenuator(thirdparty.AttenuatorConfig{
Location: "https://thirdparty.example.com",
Scheme: scheme,
CaveatIssuer: caveatIssuer,
})
```
- `Location` will be added to each caveat to hint at where to discharge the macaroon later.
- `Scheme` should be provided and match the scheme used to create the macaroon. Mixing schemes will result in undefined behavior.
- `CaveatIDIssuer` issues third party caveat IDs based on a generated caveat key and a predicate evaluated by the third party.
### Caveat ID Issuer
CaveatIDIssuer is an interface that must be implemented to construct a `thirdparty.Attenuator`.
It exchanges a `thirdparty.Ticket` which is a pair containing a CaveatKey that is randomly generated for this
macaroon, and a Predicate to be evaluated by the third party before discharges the caveat; for an opaque caveat ID
that only the third party can use later to recover the Caveat Key and Predicate.
One way to implement this is by having a third-party implement an API that can take this `thirdparty.Ticket` and
return a caveat id. This requires the third party to be active in minting a caveat, and typically would create an cId/cK.
Implementing such a protocol is out of scope for this library, but another library implementing
`thirdparty.CaveatIDIssuer` may provide it.
Another way is to use public-key cryptography to encrypt the caveat key/predicate payload.
This doesn't require a third party to be an active participant in the creation of the caveat.
Instead, the Caveat ID is constructed by encrypting the Caveat Key and Predicate using the third party's public key.
This method implemented in the `exchange` package which can be configured with Encoder/Encryptor implementations for
which the third party discharge service has a corresponding implementation for Decoder/Decryptor.
```go
issuer := exchange.CaveatIDIssuer{
Encryptor: encryptor,
Encoder: encoder,
}
```
```mermaid
flowchart TD
subgraph Add 3p Caveat
M1[/Macaroon/]
PR[/3rd Party Predicate/]
M1 ~~~ PR
M1 ~~~ M2
end
subgraph Attenuator
PR --> T[/Ticket/]
RK[/Random Source/] -- Random Data --> CK
CK[/Caveat Key/] --> T
CID[/Caveat ID/]
ECK(Encrypt Caveat Key)
CK --> ECK
M1 -- sig --> ECK
T --> E
ECK --> VID[/Verification ID/]
ECK ~~~ CID
subgraph CaveatIDIssuer
E[Encoder] -- encoded ticket --> C
C[Encryptor] -- encrypted bytes --> CID
end
VID --> APPEND
CID --> APPEND
APPEND(Append Caveat) --> M2
M2[/New Macaroon/]
end
```
Possible implementations for the `Encoder` interface:
- [example/msgpack](./example/msgpack)- Encodes using [MsgPack](https://msgpack.org/index.html)
A possible implementation for the Encryptor/Decryptor:
- [example/agecrypt](./example/agecrypt): uses [Age](https://age-encryption.org/) to encrypt third-party caveat IDs using Age Recipient.
## Requesting a Discharge Macaroon from a Third Party
The means by which requests are made to a third party service to create discharge macaroons are not defined
by the spec, but implemented by the end user. This library has some helper to make it easier to discharge
all third-party caveats recursively, returning a collection of discharge macaroons.
As long as the behavior of the discharge request can be described using the ThirdParty interface,
they can be collected into a `thirdparty.Set` which has a Discharge method taking a Macaroon and returning all
the discharge macaroons.
```go
thirdPartySet := thirdparty.Set{authThirdParty}
dischargeMacaroons, err := thirdPartySet.Discharge(ctx, &myMacaroon)
```
## Discharging Third-Party Caveats
When a Macaroon has third party caveats, they must be discharged to validate the entire macaroon stack.
The bearer of the authorization macaroon should make a request to the third-party service with the caveat ID to discharge
(likely with some authorization for that service too.)
A third-party service may use the `thirdparty.Discharger` to take a CaveatID and return a new Macaroon which will discharge the caveat.
```go
discharger, err := thirdparty.NewDischarger(thirdparty.DischargerConfig{
Location: "https://thirdparty.example.com",
Scheme: scheme,
TicketExtractor: text,
})
```
- `Location` will be added to the discharge macaroon.
- `Scheme` should be provided and match the scheme used to create the macaroon. Mixing schemes will result in undefined behavior.
- `TicketExtractor` extracts `thirdparty.Ticket` from a caveat ID.
### TicketExtractor
A TicketExtractor extracts the `thirdparty.Ticket` information from a caveat ID.
This is the "dual" of the `thirdparty.CaveatIDIssuer`
If the `CaveatIDIssuer` resulted in the third party generating an opaque caveat id and associating it
to a `thirdparty.Ticket` database, then this would look up that ticket and return it. Implementing such
a protocol is out of scope for this library, but another library implementing `thirdparty.TicketExtractor` may provide it.
If the CaveatIDIssue instead encrypted the ticket for the third party using its public key, then
this would extract the ticket from the caveat id by decrypting and decoding it.
This method implemented in the `exchange` package which can be configured with Decoder/Decryptor.
```go
text := exchange.TicketExtractor{
Decryptor: decryptor,
Decoder: decoder,
}
```
```mermaid
flowchart TD
subgraph Discharger
S((START)) --> CID
CID[/Third-party Caveat ID/] --> D[Decryptor]
subgraph TicketExtractor
D -- decrypted bytes --> DEC[Decoder]
DEC -- decoded ticket --> T[Ticket]
end
T --> P[/Predicate/]
T --> CK[/Cavate Key/]
P --> PC{Predicate Checker}
PC -- ok --> CDM(Create Discharge Macaroon)
PC -- fail --> E((ERROR))
CDM --> DCM[/Discharge Macaroon/]
CID --> DCM
CK --> DCM
DCM --> END((END))
end
```
Possible implementations for the `Decoder` interface:
- [example/msgpack](./example/msgpack) - Encodes using [MsgPack](https://msgpack.org/index.html)
A possible implementation for the `Decryptor`:
- [example/agecrypt](./example/agecrypt): uses [Age](https://age-encryption.org/) to decrypt caveat IDs using Age Identities.
### PredicateChecker
A PredicateChecker interprets a caveat id, and evaluates its result. It returns true if the predicate is satisfied.
Once satisfied, the `thirdparty.Discharger` can issue the discharge caveat from the ticket.
The implementation of the `thirdparty.PredicateChecker` is provided by the user of this library, since
a caveat id is an opaque string of bytes, without any meaning in the macaroon spec.