The task is a problem class, not a one-line bug: implement macaroon-style bearer tokens whose caveats may require third-party discharge macaroons, and enforce several independent security properties. A naive implementation gets the happy path right and fails the adversarial path, because each property is caused by a different structural decision. Root causes:
The working implementation and full write-up are saved. All 16 adversarial tests pass under -race, go vet is clean, and gofmt reports nothing.
~/SOLUTION.md~/macaroon/ (go.mod, macaroon.go, verify.go, macaroon_test.go)Below is the solution markdown.
The task is a problem class, not a one-line bug: implement macaroon-style bearer tokens whose caveats may require third-party discharge macaroons, and enforce several independent security properties. A naive implementation gets the happy path right and fails the adversarial path, because each property is caused by a different structural decision. Root causes:
| Symptom | Root cause in a naive implementation | Fix here |
|---|---|---|
| Discharge for token A accepted by token B (replanting) | Discharge is an independent bearer token; its signature isn't tied to its primary. | Bind with bind = HMAC2(0, primarySig, dischargeSig). The primary signature is unique, so a discharge only fits its own primary. |
| Self-referential / recursive discharge graphs → infinite recursion / stack overflow | Discharges resolved by id with no "already consumed" state. | verificationContext.used is set before recursing; each discharge satisfies at most one caveat. Plus a depth cap. |
| Truncated / reordered caveat chains accepted | Verifier canonicalizes the caveat set instead of requiring the exact ordered chain. | Every caveat advances one HMAC chain in slice order; the recomputed tag is compared to the presented signature. Dropping/swapping a caveat changes the tag. |
| Chosen-caveat adversary gets predicates evaluated on an unauthenticated token | check is called during chain recomputation, before the tag is verified. |
Two phases: (1) authenticate the whole discharge graph and collect conditions; (2) only then evaluate them in order. |
| Timing oracle on the signature | bytes.Equal/!= short-circuits. |
hmac.Equal for every secret comparison. |
| Non-monotone attenuation | Derived token's signature/binding not recomputed, or verifier ignores caveats. | Appending a caveat advances the chain; verifier requires all conditions, so the accepted set can only shrink. |
Let A(m) be the accepted request-context set for m with fixed discharge set D.
m+c computes sig' = H(sig,c). Any accepting execution must reproduce the whole chain, including c, and satisfy check(c), hence it also reproduces m and passes all of m's conditions, so A(m+c) ⊆ A(m). It is strict iff c is not implied by m's existing conditions (there is a context passing m and failing c). A tautological caveat shrinks nothing — that is the precise form of "strictly less powerful."d requires d.sig == HMAC2(0, primarySig, rawChain(d)). primarySig commits to primary id and every caveat, so distinct primaries have distinct bindings.findDischarge consumes an id at most once, so a finite discharge set yields a finite recursion tree; a cycle must revisit a consumed id.HMAC2 for third-party, HMAC for first-party); only the exact ordered caveat list verifies.VerificationId). VerificationId is AES-256-GCM sealed so tampering fails closed; the final tag compares in constant time.Self-contained module (standard library only). Scheme (all values 32 bytes, H(k,x)=HMAC-SHA256(k,x)):
makeKey(vk) = H("macaroons-key-generator", vk)
H2(k,a,b) = H(k, H(k,a) || H(k,b))
sig0 = H(makeKey(rootKey), id)
sig_i = H(sig_{i-1}, caveatId) first-party
sig_i = H2(sig_{i-1}, verificationId, caveatId) third-party
bind = H2(0, primarySig, dischargeSig) discharge binding
verificationId = AES-256-GCM_seal(sig_before_caveat, makeKey(sharedKey))
go.mod:
module macaroon
go 1.26
macaroon.go:
// Package macaroon implements attenuable bearer tokens (macaroons) with
// first-party caveats, third-party discharge caveats and HMAC-SHA256
// signature chaining.
//
// Signature scheme (all values are 32 bytes):
//
// sig0 = HMAC(makeKey(rootKey), id)
// sig_i = HMAC(sig_{i-1}, caveatId) first-party
// sig_i = HMAC2(sig_{i-1}, verificationId, caveatId) third-party
// bind = HMAC2(0, primarySig, dischargeSig) discharge binding
//
// where HMAC2(k, a, b) = HMAC(k, HMAC(k,a) || HMAC(k,b)) and
// makeKey(vk) = HMAC("macaroons-key-generator", vk). This mirrors the
// libmacaroons / gopkg.in/macaroon.v2 construction.
package macaroon
import (
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"errors"
"fmt"
)
const (
keyLen = 32
hashLen = sha256.Size
// maxDischargeDepth bounds recursion as defence in depth against long
// non-cyclic chains; cycles are already rejected by the used-set.
maxDischargeDepth = 64
)
var (
errInvalidSignature = errors.New("macaroon: invalid signature")
keyGen = []byte("macaroons-key-generator")
zeroKey [keyLen]byte
)
// Macaroon is an attenuable bearer token. Macaroons are mutable; Clone
// before branching an attenuation chain.
type Macaroon struct {
location string
id []byte
caveats []Caveat
sig [hashLen]byte
}
// Caveat is a first- or third-party restriction. For first-party, only Id
// (the predicate) is set. For third-party, Id is the discharge lookup id
// (must equal the discharge macaroon's Id) and VerificationId seals the
// discharge root key under the signature that preceded this caveat.
type Caveat struct {
Id []byte
VerificationId []byte
Location string
}
// New mints a root macaroon from a secret root key.
func New(rootKey, id []byte, location string) *Macaroon {
m := &Macaroon{location: location, id: append([]byte(nil), id...)}
k := makeKey(rootKey)
m.sig = keyedHash(&k, m.id)
return m
}
// Clone returns a deep copy of m.
func (m *Macaroon) Clone() *Macaroon {
m1 := *m
m1.id = append([]byte(nil), m.id...)
m1.caveats = make([]Caveat, len(m.caveats))
for i, c := range m.caveats {
m1.caveats[i] = Caveat{
Id: append([]byte(nil), c.Id...),
VerificationId: append([]byte(nil), c.VerificationId...),
Location: c.Location,
}
}
return &m1
}
func (m *Macaroon) Id() []byte { return append([]byte(nil), m.id...) }
func (m *Macaroon) Location() string { return m.location }
// Signature returns a copy of the current signature.
func (m *Macaroon) Signature() []byte {
s := make([]byte, hashLen)
copy(s, m.sig[:])
return s
}
// Caveats returns a deep copy of the caveat slice.
func (m *Macaroon) Caveats() []Caveat {
cs := make([]Caveat, len(m.caveats))
for i, c := range m.caveats {
cs[i] = Caveat{
Id: append([]byte(nil), c.Id...),
VerificationId: append([]byte(nil), c.VerificationId...),
Location: c.Location,
}
}
return cs
}
// AddFirstPartyCaveat appends a predicate the target service must satisfy.
func (m *Macaroon) AddFirstPartyCaveat(condition []byte) {
m.caveats = append(m.caveats, Caveat{Id: append([]byte(nil), condition...)})
m.sig = keyedHash(&m.sig, condition)
}
// AddThirdPartyCaveat appends a caveat discharged by a third party holding
// sharedKey. caveatId must be the Id of the discharge macaroon.
func (m *Macaroon) AddThirdPartyCaveat(sharedKey, caveatId []byte, loc string) error {
dischargeRootKey := makeKey(sharedKey)
vid, err := encrypt(&m.sig, &dischargeRootKey)
if err != nil {
return fmt.Errorf("macaroon: sealing discharge key: %w", err)
}
m.caveats = append(m.caveats, Caveat{
Id: append([]byte(nil), caveatId...),
VerificationId: vid,
Location: loc,
})
m.sig = keyedHash2(&m.sig, vid, caveatId)
return nil
}
// Bind ties a discharge to the final signature of its primary, defeating
// replanting onto a sibling token.
func (m *Macaroon) Bind(primarySig []byte) {
m.sig = bindForRequest(primarySig, m.sig[:])
}
// makeKey derives a fixed-length key from an arbitrary-length root key.
func makeKey(variableKey []byte) [keyLen]byte {
h := hmac.New(sha256.New, keyGen)
h.Write(variableKey)
var k [keyLen]byte
copy(k[:], h.Sum(nil))
return k
}
// keyedHash is HMAC-SHA256(key, text).
func keyedHash(key *[keyLen]byte, text []byte) [hashLen]byte {
h := hmac.New(sha256.New, key[:])
h.Write(text)
var sum [hashLen]byte
copy(sum[:], h.Sum(nil))
return sum
}
// keyedHash2 is HMAC(k, HMAC(k,a) || HMAC(k,b)).
func keyedHash2(key *[keyLen]byte, a, b []byte) [hashLen]byte {
ha := keyedHash(key, a)
hb := keyedHash(key, b)
var data [hashLen * 2]byte
copy(data[:hashLen], ha[:])
copy(data[hashLen:], hb[:])
return keyedHash(key, data[:])
}
// bindForRequest binds a discharge signature to a primary signature. The
// zero-key prefix domain-separates it; comparison uses hmac.Equal.
func bindForRequest(primarySig, dischargeSig []byte) [hashLen]byte {
if hmac.Equal(primarySig, dischargeSig) {
var s [hashLen]byte
copy(s[:], dischargeSig)
return s
}
return keyedHash2(&zeroKey, primarySig, dischargeSig)
}
// encrypt seals a 32-byte discharge root key under the macaroon signature.
func encrypt(key *[keyLen]byte, plaintext *[hashLen]byte) ([]byte, error) {
block, err := aes.NewCipher(key[:])
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
out := make([]byte, len(nonce))
copy(out, nonce)
return gcm.Seal(out, nonce, plaintext[:], nil), nil
}
// decrypt reverses encrypt; it authenticates the ciphertext so tampering
// fails closed instead of yielding a wrong root key.
func decrypt(key *[keyLen]byte, ciphertext []byte) ([hashLen]byte, error) {
var out [hashLen]byte
block, err := aes.NewCipher(key[:])
if err != nil {
return out, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return out, err
}
ns := gcm.NonceSize()
if len(ciphertext) < ns+gcm.Overhead() {
return out, errors.New("macaroon: verification id too short")
}
nonce, ct := ciphertext[:ns], ciphertext[ns:]
pt, err := gcm.Open(nil, nonce, ct, nil)
if err != nil {
return out, errors.New("macaroon: verification id authentication failed")
}
if len(pt) != hashLen {
return out, errors.New("macaroon: discharge key has wrong length")
}
copy(out[:], pt)
return out, nil
}
verify.go:
package macaroon
import (
"bytes"
"crypto/hmac"
"fmt"
)
// Verification is two-phase:
// 1. authenticate recomputes every HMAC chain, resolves and binds every
// discharge, rejects cycles/duplicates and collects first-party caveat
// conditions in traversal order without invoking user code.
// 2. conditions are handed to the caller's check in that exact order.
//
// MAC check before policy check means a chosen-caveat adversary can never
// make the verifier evaluate attacker-influenced predicates on an
// unauthenticated token.
type verificationContext struct {
used []bool
discharges []*Macaroon
primarySig []byte
conditions []string
}
// Verify authenticates m against rootKey, resolves discharges, then
// evaluates every first-party caveat with check (non-nil error = deny).
// Every supplied discharge must be used exactly once. Conditions are
// evaluated in attenuation order, depth-first into discharges.
func (m *Macaroon) Verify(rootKey []byte, check func(condition string) error, discharges []*Macaroon) error {
if check == nil {
check = func(string) error { return nil }
}
vc := &verificationContext{
discharges: discharges,
used: make([]bool, len(discharges)),
primarySig: m.Signature(),
}
k := makeKey(rootKey)
if err := vc.authenticate(m, 0, 0, &k); err != nil {
return err
}
for i, used := range vc.used {
if !used {
return fmt.Errorf("macaroon: discharge macaroon %q was not used", vc.discharges[i].id)
}
}
for _, cond := range vc.conditions {
if err := check(cond); err != nil {
return err
}
}
return nil
}
func (vc *verificationContext) authenticate(m *Macaroon, index, depth int, rootKey *[keyLen]byte) error {
if depth > maxDischargeDepth {
return fmt.Errorf("macaroon: discharge chain exceeds depth %d", maxDischargeDepth)
}
caveatSig := keyedHash(rootKey, m.id)
for i, cav := range m.caveats {
if len(cav.VerificationId) > 0 {
// Discharge key was sealed under the signature preceding
// this caveat, so decrypt before advancing.
dischargeKey, err := decrypt(&caveatSig, cav.VerificationId)
if err != nil {
return fmt.Errorf("macaroon: caveat %d: cannot recover discharge key: %w", i, err)
}
dm, di, err := vc.findDischarge(cav.Id)
if err != nil {
return err
}
if err := vc.authenticate(dm, di+1, depth+1, &dischargeKey); err != nil {
return err
}
caveatSig = keyedHash2(&caveatSig, cav.VerificationId, cav.Id)
continue
}
caveatSig = keyedHash(&caveatSig, cav.Id)
vc.conditions = append(vc.conditions, string(cav.Id))
}
if index > 0 {
caveatSig = bindForRequest(vc.primarySig, caveatSig[:])
}
// Constant-time comparison: never leak how many leading bytes matched.
if !hmac.Equal(caveatSig[:], m.sig[:]) {
return errInvalidSignature
}
return nil
}
// findDischarge returns the discharge whose id equals id and marks it used.
// The used-set makes cyclic/self-referential graphs terminate.
func (vc *verificationContext) findDischarge(id []byte) (*Macaroon, int, error) {
for i, dm := range vc.discharges {
if dm == nil || !bytes.Equal(dm.id, id) {
continue
}
if vc.used[i] {
return nil, 0, fmt.Errorf("macaroon: discharge %q used more than once (cycle or duplicate caveat)", id)
}
vc.used[i] = true
return dm, i, nil
}
return nil, 0, fmt.Errorf("macaroon: no discharge macaroon for caveat %q", id)
}
The adversarial suite macaroon_test.go (16 tests) is on disk alongside these files; it covers order/short-circuit, replanting, self-reference, mutual recursion, truncation, reordering, monotone attenuation, bit-flip forgeries, wrong root, tampered VerificationId, and duplicate/unused discharges.
cd ~/macaroon
gofmt -l . # prints nothing
go vet ./... # clean
go test -race -count=1 -v ./...
Observed (Go 1.26, linux/amd64):
=== RUN TestFirstPartyChainAndOrder
--- PASS: TestFirstPartyChainAndOrder
=== RUN TestSatisfactionOrderShortCircuits
--- PASS: TestSatisfactionOrderShortCircuits
=== RUN TestThirdPartyDischarge
--- PASS: TestThirdPartyDischarge
=== RUN TestDischargeCannotBeReplantedOnSibling
--- PASS: TestDischargeCannotBeReplantedOnSibling
=== RUN TestDischargeBoundToPrimaryFinalSignature
--- PASS: TestDischargeBoundToPrimaryFinalSignature
=== RUN TestSelfReferentialDischarge
--- PASS: TestSelfReferentialDischarge
=== RUN TestMutuallyRecursiveDischarges
--- PASS: TestMutuallyRecursiveDischarges
=== RUN TestTruncatedCaveatChainRejected
--- PASS: TestTruncatedCaveatChainRejected
=== RUN TestReorderedCaveatChainRejected
--- PASS: TestReorderedCaveatChainRejected
=== RUN TestTruncatedDischargeRejected
--- PASS: TestTruncatedDischargeRejected
=== RUN TestAttenuationIsMonotone
--- PASS: TestAttenuationIsMonotone
=== RUN TestSignatureForgeriesRejected
--- PASS: TestSignatureForgeriesRejected
=== RUN TestWrongRootKeyRejected
--- PASS: TestWrongRootKeyRejected
=== RUN TestTamperedVerificationIdRejected
--- PASS: TestTamperedVerificationIdRejected
=== RUN TestUnusedAndDuplicateDischargesRejected
--- PASS: TestUnusedAndDuplicateDischargesRejected
=== RUN TestNestedDischarges
--- PASS: TestNestedDischarges
PASS
ok macaroon 1.015s
| Requirement | Test |
|---|---|
| Satisfaction order + short-circuit | TestFirstPartyChainAndOrder, TestSatisfactionOrderShortCircuits, TestThirdPartyDischarge |
| Third-party discharge | TestThirdPartyDischarge, TestNestedDischarges |
| No replanting | TestDischargeCannotBeReplantedOnSibling, TestDischargeBoundToPrimaryFinalSignature |
| Cycles / self-reference | TestSelfReferentialDischarge, TestMutuallyRecursiveDischarges |
| Truncation / reordering | TestTruncatedCaveatChainRejected, TestReorderedCaveatChainRejected, TestTruncatedDischargeRejected |
| Monotone (strict) attenuation | TestAttenuationIsMonotone |
| Constant-time / chosen-caveat unforgeability | hmac.Equal in verify.go; TestSignatureForgeriesRejected, TestWrongRootKeyRejected, TestTamperedVerificationIdRejected |
| Discharge replay / unused | TestUnusedAndDuplicateDischargesRejected |
Optional fuzz amplification (add to macaroon_test.go):
func FuzzNoTamperAccepted(f *testing.F) {
f.Add([]byte("root"), []byte("id"), []byte("a=1"))
f.Fuzz(func(t *testing.T, root, id, cond []byte) {
m := New(root, id, "loc")
m.AddFirstPartyCaveat(cond)
if m.Verify(root, func(string) error { return nil }, nil) != nil {
t.Fatal("freshly minted macaroon failed")
}
for i := range m.sig {
c := m.Clone()
c.sig[i] ^= 1
if c.Verify(root, func(string) error { return nil }, nil) == nil {
t.Fatalf("tampered signature accepted at byte %d", i)
}
}
})
}
go test -run '^$' -fuzz FuzzNoTamperAccepted -fuzztime 30s
root := []byte("bank-root-key")
shared := []byte("shared-with-auth-service")
m := macaroon.New(root, []byte("invoice/42"), "bank.example")
_ = m.AddThirdPartyCaveat(shared, []byte("auth/verified-email"), "auth.example")
m.AddFirstPartyCaveat([]byte("role=user"))
d := macaroon.New(shared, []byte("auth/verified-email"), "auth.example")
d.AddFirstPartyCaveat([]byte("email=verified"))
d.Bind(m.Signature())
err := m.Verify(root, func(condition string) error {
// Evaluate condition against trusted request state; return an error to deny.
return nil
}, []*macaroon.Macaroon{d})
# Evidence - Problem class: go-macaroon-third-party-caveat-discharge-cycle-attenuation - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-29T10:07:15.221Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Implement macaroon-style bearer tokens where a root key mints an attenuable token whose caveats may require third-party discharge macaroons: verification must enforce caveat satisfaction order, bind each discharge to the root signature so discharges cannot be replanted onto a sibling token, detect cyclic and self-referential discharge graphs, and reject truncated or reordered caveat chains. Prove attenuation is monotone \u2014 any token derived by adding caveats must be strictly less powerful than its parent for the same discharge set \u2014 while keeping HMAC chaining constant-time and unforgeable under a chosen-caveat adversary.", "environment": "go1.26", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-macaroon-third-party-caveat-discharge-cycle-attenuation", "provider": "openrouter", "solved_at": "2026-09-29T10:07:15.222Z", "version": "1.26"}The working implementation and full write-up are saved. All 16 adversarial tests pass under -race, go vet is clean, and gofmt reports nothing.
~/SOLUTION.md~/macaroon/ (go.mod, macaroon.go, verify.go, macaroon_test.go)Below is the solution markdown.
The task is a problem class, not a one-line bug: implement macaroon-style bearer tokens whose caveats may require third-party discharge macaroons, and enforce several independent security properties. A naive implementation gets the happy path right and fails the adversarial path, because each property is caused by a different structural decision. Root causes:
| Symptom | Root cause in a naive implementation | Fix here |
|---|---|---|
| Discharge for token A accepted by token B (replanting) | Discharge is an independent bearer token; its signature isn't tied to its primary. | Bind with bind = HMAC2(0, primarySig, dischargeSig). The primary signature is unique, so a discharge only fits its own primary. |
| Self-referential / recursive discharge graphs → infinite recursion / stack overflow | Discharges resolved by id with no "already consumed" state. | verificationContext.used is set before recursing; each discharge satisfies at most one caveat. Plus a depth cap. |
| Truncated / reordered caveat chains accepted | Verifier canonicalizes the caveat set instead of requiring the exact ordered chain. | Every caveat advances one HMAC chain in slice order; the recomputed tag is compared to the presented signature. Dropping/swapping a caveat changes the tag. |
| Chosen-caveat adversary gets predicates evaluated on an unauthenticated token | check is called during chain recomputation, before the tag is verified. |
Two phases: (1) authenticate the whole discharge graph and collect conditions; (2) only then evaluate them in order. |
| Timing oracle on the signature | bytes.Equal/!= short-circuits. |
hmac.Equal for every secret comparison. |
| Non-monotone attenuation | Derived token's signature/binding not recomputed, or verifier ignores caveats. | Appending a caveat advances the chain; verifier requires all conditions, so the accepted set can only shrink. |
Let A(m) be the accepted request-context set for m with fixed discharge set D.
m+c computes sig' = H(sig,c). Any accepting execution must reproduce the whole chain, including c, and satisfy check(c), hence it also reproduces m and passes all of m's conditions, so A(m+c) ⊆ A(m). It is strict iff c is not implied by m's existing conditions (there is a context passing m and failing c). A tautological caveat shrinks nothing — that is the precise form of "strictly less powerful."d requires d.sig == HMAC2(0, primarySig, rawChain(d)). primarySig commits to primary id and every caveat, so distinct primaries have distinct bindings.findDischarge consumes an id at most once, so a finite discharge set yields a finite recursion tree; a cycle must revisit a consumed id.HMAC2 for third-party, HMAC for first-party); only the exact ordered caveat list verifies.VerificationId). VerificationId is AES-256-GCM sealed so tampering fails closed; the final tag compares in constant time.Self-contained module (standard library only). Scheme (all values 32 bytes, H(k,x)=HMAC-SHA256(k,x)):
makeKey(vk) = H("macaroons-key-generator", vk)
H2(k,a,b) = H(k, H(k,a) || H(k,b))
sig0 = H(makeKey(rootKey), id)
sig_i = H(sig_{i-1}, caveatId) first-party
sig_i = H2(sig_{i-1}, verificationId, caveatId) third-party
bind = H2(0, primarySig, dischargeSig) discharge binding
verificationId = AES-256-GCM_seal(sig_before_caveat, makeKey(sharedKey))
go.mod:
module macaroon
go 1.26
macaroon.go:
// Package macaroon implements attenuable bearer tokens (macaroons) with
// first-party caveats, third-party discharge caveats and HMAC-SHA256
// signature chaining.
//
// Signature scheme (all values are 32 bytes):
//
// sig0 = HMAC(makeKey(rootKey), id)
// sig_i = HMAC(sig_{i-1}, caveatId) first-party
// sig_i = HMAC2(sig_{i-1}, verificationId, caveatId) third-party
// bind = HMAC2(0, primarySig, dischargeSig) discharge binding
//
// where HMAC2(k, a, b) = HMAC(k, HMAC(k,a) || HMAC(k,b)) and
// makeKey(vk) = HMAC("macaroons-key-generator", vk). This mirrors the
// libmacaroons / gopkg.in/macaroon.v2 construction.
package macaroon
import (
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"errors"
"fmt"
)
const (
keyLen = 32
hashLen = sha256.Size
// maxDischargeDepth bounds recursion as defence in depth against long
// non-cyclic chains; cycles are already rejected by the used-set.
maxDischargeDepth = 64
)
var (
errInvalidSignature = errors.New("macaroon: invalid signature")
keyGen = []byte("macaroons-key-generator")
zeroKey [keyLen]byte
)
// Macaroon is an attenuable bearer token. Macaroons are mutable; Clone
// before branching an attenuation chain.
type Macaroon struct {
location string
id []byte
caveats []Caveat
sig [hashLen]byte
}
// Caveat is a first- or third-party restriction. For first-party, only Id
// (the predicate) is set. For third-party, Id is the discharge lookup id
// (must equal the discharge macaroon's Id) and VerificationId seals the
// discharge root key under the signature that preceded this caveat.
type Caveat struct {
Id []byte
VerificationId []byte
Location string
}
// New mints a root macaroon from a secret root key.
func New(rootKey, id []byte, location string) *Macaroon {
m := &Macaroon{location: location, id: append([]byte(nil), id...)}
k := makeKey(rootKey)
m.sig = keyedHash(&k, m.id)
return m
}
// Clone returns a deep copy of m.
func (m *Macaroon) Clone() *Macaroon {
m1 := *m
m1.id = append([]byte(nil), m.id...)
m1.caveats = make([]Caveat, len(m.caveats))
for i, c := range m.caveats {
m1.caveats[i] = Caveat{
Id: append([]byte(nil), c.Id...),
VerificationId: append([]byte(nil), c.VerificationId...),
Location: c.Location,
}
}
return &m1
}
func (m *Macaroon) Id() []byte { return append([]byte(nil), m.id...) }
func (m *Macaroon) Location() string { return m.location }
// Signature returns a copy of the current signature.
func (m *Macaroon) Signature() []byte {
s := make([]byte, hashLen)
copy(s, m.sig[:])
return s
}
// Caveats returns a deep copy of the caveat slice.
func (m *Macaroon) Caveats() []Caveat {
cs := make([]Caveat, len(m.caveats))
for i, c := range m.caveats {
cs[i] = Caveat{
Id: append([]byte(nil), c.Id...),
VerificationId: append([]byte(nil), c.VerificationId...),
Location: c.Location,
}
}
return cs
}
// AddFirstPartyCaveat appends a predicate the target service must satisfy.
func (m *Macaroon) AddFirstPartyCaveat(condition []byte) {
m.caveats = append(m.caveats, Caveat{Id: append([]byte(nil), condition...)})
m.sig = keyedHash(&m.sig, condition)
}
// AddThirdPartyCaveat appends a caveat discharged by a third party holding
// sharedKey. caveatId must be the Id of the discharge macaroon.
func (m *Macaroon) AddThirdPartyCaveat(sharedKey, caveatId []byte, loc string) error {
dischargeRootKey := makeKey(sharedKey)
vid, err := encrypt(&m.sig, &dischargeRootKey)
if err != nil {
return fmt.Errorf("macaroon: sealing discharge key: %w", err)
}
m.caveats = append(m.caveats, Caveat{
Id: append([]byte(nil), caveatId...),
VerificationId: vid,
Location: loc,
})
m.sig = keyedHash2(&m.sig, vid, caveatId)
return nil
}
// Bind ties a discharge to the final signature of its primary, defeating
// replanting onto a sibling token.
func (m *Macaroon) Bind(primarySig []byte) {
m.sig = bindForRequest(primarySig, m.sig[:])
}
// makeKey derives a fixed-length key from an arbitrary-length root key.
func makeKey(variableKey []byte) [keyLen]byte {
h := hmac.New(sha256.New, keyGen)
h.Write(variableKey)
var k [keyLen]byte
copy(k[:], h.Sum(nil))
return k
}
// keyedHash is HMAC-SHA256(key, text).
func keyedHash(key *[keyLen]byte, text []byte) [hashLen]byte {
h := hmac.New(sha256.New, key[:])
h.Write(text)
var sum [hashLen]byte
copy(sum[:], h.Sum(nil))
return sum
}
// keyedHash2 is HMAC(k, HMAC(k,a) || HMAC(k,b)).
func keyedHash2(key *[keyLen]byte, a, b []byte) [hashLen]byte {
ha := keyedHash(key, a)
hb := keyedHash(key, b)
var data [hashLen * 2]byte
copy(data[:hashLen], ha[:])
copy(data[hashLen:], hb[:])
return keyedHash(key, data[:])
}
// bindForRequest binds a discharge signature to a primary signature. The
// zero-key prefix domain-separates it; comparison uses hmac.Equal.
func bindForRequest(primarySig, dischargeSig []byte) [hashLen]byte {
if hmac.Equal(primarySig, dischargeSig) {
var s [hashLen]byte
copy(s[:], dischargeSig)
return s
}
return keyedHash2(&zeroKey, primarySig, dischargeSig)
}
// encrypt seals a 32-byte discharge root key under the macaroon signature.
func encrypt(key *[keyLen]byte, plaintext *[hashLen]byte) ([]byte, error) {
block, err := aes.NewCipher(key[:])
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
out := make([]byte, len(nonce))
copy(out, nonce)
return gcm.Seal(out, nonce, plaintext[:], nil), nil
}
// decrypt reverses encrypt; it authenticates the ciphertext so tampering
// fails closed instead of yielding a wrong root key.
func decrypt(key *[keyLen]byte, ciphertext []byte) ([hashLen]byte, error) {
var out [hashLen]byte
block, err := aes.NewCipher(key[:])
if err != nil {
return out, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return out, err
}
ns := gcm.NonceSize()
if len(ciphertext) < ns+gcm.Overhead() {
return out, errors.New("macaroon: verification id too short")
}
nonce, ct := ciphertext[:ns], ciphertext[ns:]
pt, err := gcm.Open(nil, nonce, ct, nil)
if err != nil {
return out, errors.New("macaroon: verification id authentication failed")
}
if len(pt) != hashLen {
return out, errors.New("macaroon: discharge key has wrong length")
}
copy(out[:], pt)
return out, nil
}
verify.go:
package macaroon
import (
"bytes"
"crypto/hmac"
"fmt"
)
// Verification is two-phase:
// 1. authenticate recomputes every HMAC chain, resolves and binds every
// discharge, rejects cycles/duplicates and collects first-party caveat
// conditions in traversal order without invoking user code.
// 2. conditions are handed to the caller's check in that exact order.
//
// MAC check before policy check means a chosen-caveat adversary can never
// make the verifier evaluate attacker-influenced predicates on an
// unauthenticated token.
type verificationContext struct {
used []bool
discharges []*Macaroon
primarySig []byte
conditions []string
}
// Verify authenticates m against rootKey, resolves discharges, then
// evaluates every first-party caveat with check (non-nil error = deny).
// Every supplied discharge must be used exactly once. Conditions are
// evaluated in attenuation order, depth-first into discharges.
func (m *Macaroon) Verify(rootKey []byte, check func(condition string) error, discharges []*Macaroon) error {
if check == nil {
check = func(string) error { return nil }
}
vc := &verificationContext{
discharges: discharges,
used: make([]bool, len(discharges)),
primarySig: m.Signature(),
}
k := makeKey(rootKey)
if err := vc.authenticate(m, 0, 0, &k); err != nil {
return err
}
for i, used := range vc.used {
if !used {
return fmt.Errorf("macaroon: discharge macaroon %q was not used", vc.discharges[i].id)
}
}
for _, cond := range vc.conditions {
if err := check(cond); err != nil {
return err
}
}
return nil
}
func (vc *verificationContext) authenticate(m *Macaroon, index, depth int, rootKey *[keyLen]byte) error {
if depth > maxDischargeDepth {
return fmt.Errorf("macaroon: discharge chain exceeds depth %d", maxDischargeDepth)
}
caveatSig := keyedHash(rootKey, m.id)
for i, cav := range m.caveats {
if len(cav.VerificationId) > 0 {
// Discharge key was sealed under the signature preceding
// this caveat, so decrypt before advancing.
dischargeKey, err := decrypt(&caveatSig, cav.VerificationId)
if err != nil {
return fmt.Errorf("macaroon: caveat %d: cannot recover discharge key: %w", i, err)
}
dm, di, err := vc.findDischarge(cav.Id)
if err != nil {
return err
}
if err := vc.authenticate(dm, di+1, depth+1, &dischargeKey); err != nil {
return err
}
caveatSig = keyedHash2(&caveatSig, cav.VerificationId, cav.Id)
continue
}
caveatSig = keyedHash(&caveatSig, cav.Id)
vc.conditions = append(vc.conditions, string(cav.Id))
}
if index > 0 {
caveatSig = bindForRequest(vc.primarySig, caveatSig[:])
}
// Constant-time comparison: never leak how many leading bytes matched.
if !hmac.Equal(caveatSig[:], m.sig[:]) {
return errInvalidSignature
}
return nil
}
// findDischarge returns the discharge whose id equals id and marks it used.
// The used-set makes cyclic/self-referential graphs terminate.
func (vc *verificationContext) findDischarge(id []byte) (*Macaroon, int, error) {
for i, dm := range vc.discharges {
if dm == nil || !bytes.Equal(dm.id, id) {
continue
}
if vc.used[i] {
return nil, 0, fmt.Errorf("macaroon: discharge %q used more than once (cycle or duplicate caveat)", id)
}
vc.used[i] = true
return dm, i, nil
}
return nil, 0, fmt.Errorf("macaroon: no discharge macaroon for caveat %q", id)
}
The adversarial suite macaroon_test.go (16 tests) is on disk alongside these files; it covers order/short-circuit, replanting, self-reference, mutual recursion, truncation, reordering, monotone attenuation, bit-flip forgeries, wrong root, tampered VerificationId, and duplicate/unused discharges.
cd ~/macaroon
gofmt -l . # prints nothing
go vet ./... # clean
go test -race -count=1 -v ./...
Observed (Go 1.26, linux/amd64):
=== RUN TestFirstPartyChainAndOrder
--- PASS: TestFirstPartyChainAndOrder
=== RUN TestSatisfactionOrderShortCircuits
--- PASS: TestSatisfactionOrderShortCircuits
=== RUN TestThirdPartyDischarge
--- PASS: TestThirdPartyDischarge
=== RUN TestDischargeCannotBeReplantedOnSibling
--- PASS: TestDischargeCannotBeReplantedOnSibling
=== RUN TestDischargeBoundToPrimaryFinalSignature
--- PASS: TestDischargeBoundToPrimaryFinalSignature
=== RUN TestSelfReferentialDischarge
--- PASS: TestSelfReferentialDischarge
=== RUN TestMutuallyRecursiveDischarges
--- PASS: TestMutuallyRecursiveDischarges
=== RUN TestTruncatedCaveatChainRejected
--- PASS: TestTruncatedCaveatChainRejected
=== RUN TestReorderedCaveatChainRejected
--- PASS: TestReorderedCaveatChainRejected
=== RUN TestTruncatedDischargeRejected
--- PASS: TestTruncatedDischargeRejected
=== RUN TestAttenuationIsMonotone
--- PASS: TestAttenuationIsMonotone
=== RUN TestSignatureForgeriesRejected
--- PASS: TestSignatureForgeriesRejected
=== RUN TestWrongRootKeyRejected
--- PASS: TestWrongRootKeyRejected
=== RUN TestTamperedVerificationIdRejected
--- PASS: TestTamperedVerificationIdRejected
=== RUN TestUnusedAndDuplicateDischargesRejected
--- PASS: TestUnusedAndDuplicateDischargesRejected
=== RUN TestNestedDischarges
--- PASS: TestNestedDischarges
PASS
ok macaroon 1.015s
| Requirement | Test |
|---|---|
| Satisfaction order + short-circuit | TestFirstPartyChainAndOrder, TestSatisfactionOrderShortCircuits, TestThirdPartyDischarge |
| Third-party discharge | TestThirdPartyDischarge, TestNestedDischarges |
| No replanting | TestDischargeCannotBeReplantedOnSibling, TestDischargeBoundToPrimaryFinalSignature |
| Cycles / self-reference | TestSelfReferentialDischarge, TestMutuallyRecursiveDischarges |
| Truncation / reordering | TestTruncatedCaveatChainRejected, TestReorderedCaveatChainRejected, TestTruncatedDischargeRejected |
| Monotone (strict) attenuation | TestAttenuationIsMonotone |
| Constant-time / chosen-caveat unforgeability | hmac.Equal in verify.go; TestSignatureForgeriesRejected, TestWrongRootKeyRejected, TestTamperedVerificationIdRejected |
| Discharge replay / unused | TestUnusedAndDuplicateDischargesRejected |
Optional fuzz amplification (add to macaroon_test.go):
func FuzzNoTamperAccepted(f *testing.F) {
f.Add([]byte("root"), []byte("id"), []byte("a=1"))
f.Fuzz(func(t *testing.T, root, id, cond []byte) {
m := New(root, id, "loc")
m.AddFirstPartyCaveat(cond)
if m.Verify(root, func(string) error { return nil }, nil) != nil {
t.Fatal("freshly minted macaroon failed")
}
for i := range m.sig {
c := m.Clone()
c.sig[i] ^= 1
if c.Verify(root, func(string) error { return nil }, nil) == nil {
t.Fatalf("tampered signature accepted at byte %d", i)
}
}
})
}
go test -run '^$' -fuzz FuzzNoTamperAccepted -fuzztime 30s
root := []byte("bank-root-key")
shared := []byte("shared-with-auth-service")
m := macaroon.New(root, []byte("invoice/42"), "bank.example")
_ = m.AddThirdPartyCaveat(shared, []byte("auth/verified-email"), "auth.example")
m.AddFirstPartyCaveat([]byte("role=user"))
d := macaroon.New(shared, []byte("auth/verified-email"), "auth.example")
d.AddFirstPartyCaveat([]byte("email=verified"))
d.Bind(m.Signature())
err := m.Verify(root, func(condition string) error {
// Evaluate condition against trusted request state; return an error to deny.
return nil
}, []*macaroon.Macaroon{d})
# Evidence - Problem class: go-macaroon-third-party-caveat-discharge-cycle-attenuation - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-29T10:07:15.221Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Implement macaroon-style bearer tokens where a root key mints an attenuable token whose caveats may require third-party discharge macaroons: verification must enforce caveat satisfaction order, bind each discharge to the root signature so discharges cannot be replanted onto a sibling token, detect cyclic and self-referential discharge graphs, and reject truncated or reordered caveat chains. Prove attenuation is monotone \u2014 any token derived by adding caveats must be strictly less powerful than its parent for the same discharge set \u2014 while keeping HMAC chaining constant-time and unforgeable under a chosen-caveat adversary.", "environment": "go1.26", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-macaroon-third-party-caveat-discharge-cycle-attenuation", "provider": "openrouter", "solved_at": "2026-09-29T10:07:15.222Z", "version": "1.26"}