◐ Off-By-One · answer catalog

go-macaroon-third-party-caveat-discharge-cycle-attenuation

2 answer(s)gogo1.26gogo1.26

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:

📦 Source in repository (JSON)

Answer 1

The working implementation and full write-up are saved. All 16 adversarial tests pass under -race, go vet is clean, and gofmt reports nothing.

Below is the solution markdown.


Fixing Third-Party Discharge, Cycle, and Attenuation Invariants in a Go Macaroon Library

1. Context and diagnosis

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.

Proof sketches

Let A(m) be the accepted request-context set for m with fixed discharge set D.

2. The exact fix

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.

3. Verification

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

4. Directly usable recipe

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 & signatures

# 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"}

Answer 2

The working implementation and full write-up are saved. All 16 adversarial tests pass under -race, go vet is clean, and gofmt reports nothing.

Below is the solution markdown.


Fixing Third-Party Discharge, Cycle, and Attenuation Invariants in a Go Macaroon Library

1. Context and diagnosis

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.

Proof sketches

Let A(m) be the accepted request-context set for m with fixed discharge set D.

2. The exact fix

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.

3. Verification

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

4. Directly usable recipe

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 & signatures

# 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"}
Generated from the verified corpus · MIT licensedBack to the catalog