Skip to content

aperture: support third-party caveat discharge macaroons#232

Open
djkazic wants to merge 1 commit into
lightninglabs:masterfrom
djkazic:fix-discharge-macaroons
Open

aperture: support third-party caveat discharge macaroons#232
djkazic wants to merge 1 commit into
lightninglabs:masterfrom
djkazic:fix-discharge-macaroons

Conversation

@djkazic
Copy link
Copy Markdown
Contributor

@djkazic djkazic commented Apr 15, 2026

When a macaroon contains a third-party caveat, the macaroon library's VerifySignature walks the caveat chain and calls findDischarge to locate the corresponding discharge macaroon. With nil discharges, this search always fails, returning "cannot find discharge macaroon for caveat ..." which is propagated as a verification error. The library treats a missing discharge as a verification failure, not a skippable condition, so there was no way to verify such macaroons.

The fix uses macaroon.Slice (where the first element is the root macaroon and the rest are discharges) as the serialization format in both the base64 Authorization header and the hex-encoded Grpc-Metadata-Macaroon/Macaroon headers. Slice.UnmarshalBinary is a strict superset of Macaroon.UnmarshalBinary for single-macaroon inputs, so existing clients are unaffected.

@djkazic
Copy link
Copy Markdown
Contributor Author

djkazic commented Apr 15, 2026

@claude review this

Copy link
Copy Markdown
Collaborator

@lightninglabs-deploy lightninglabs-deploy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid PR. The approach of using macaroon.Slice as the serialization format is clean and the backwards compatibility story is well thought out.

I verified that Slice.MarshalBinary for a single macaroon produces identical bytes to Macaroon.MarshalBinary (both use appendBinary under the hood), and Slice.UnmarshalBinary is indeed a strict superset of Macaroon.UnmarshalBinary — so existing single-macaroon clients are truly unaffected. 👍

A few observations/minor nits:

1. tokenFromContext discards discharges

In l402/server_interceptor.go, the new _ blank identifier at the call site means that if anyone later needs the token ID from a request that also carries discharges, they'd have to refactor. This is fine for now since tokenFromContext only needs the token ID, but worth noting.

2. TestSetHeaderBackwardsCompatible — consider also checking Slice.MarshalBinary directly

The test currently round-trips through SetHeader/header parsing to check the base64 matches. It might be slightly more robust to also assert:

sliceBytes, _ := macaroon.Slice{mac}.MarshalBinary()
require.Equal(t, macBytes, sliceBytes)

This would make the invariant the test relies on explicit, rather than testing it indirectly through the header format string.

3. HasCaveat only checks root macaroon caveats — correct

Just confirming: HasCaveat iterates m.Caveats() which returns first-party caveats only on the root macaroon, so the preimage extraction in the hex-header path correctly ignores discharge macaroons. Good.

4. Tests are thorough

Nice coverage of:

  • Auth header with/without discharges
  • Hex header with/without discharges
  • Multiple discharge round-trip
  • Backwards compatibility
  • Mint-level verification with/without discharges

The mint test correctly validates both the positive case (discharge present → verification succeeds) and the negative case (no discharge → "cannot find discharge" error).

Overall this is a clean, well-scoped change. LGTM.

Copy link
Copy Markdown
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🐴

Comment thread l402/header.go
"unmarshal macaroon: %v", err)

// Use Slice to unmarshal so we can extract discharge
// macaroons if present. By convention the first macaroon
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, so now there'll be multiple macaroons, space delimited as part of the header? Do we need to update the spec to account for this?

Comment thread l402/header.go
mac := slice[0]
var discharges []*macaroon.Macaroon
if len(slice) > 1 {
discharges = slice[1:]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re spec again, right now depending on an implicit ordering here.

Comment thread l402/header.go
mac := slice[0]
var discharges []*macaroon.Macaroon
if len(slice) > 1 {
discharges = slice[1:]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same logic in a few areas, can extract into a helper functio.

Comment thread l402/header.go
// serialized alongside the root macaroon using the macaroon.Slice convention.
func SetHeader(header *http.Header, mac *macaroon.Macaroon,
preimage fmt.Stringer) error {
preimage fmt.Stringer, discharges []*macaroon.Macaroon) error {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make the discharges ...*macaroon.Macaroon`, then we won't break the API.

Comment thread l402/header.go
Comment on lines 71 to +72
if len(matches) != 4 {
return nil, lntypes.Preimage{}, fmt.Errorf("invalid "+
"auth header format: %s", authHeader)
return nil, lntypes.Preimage{}, nil,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly out of scope, but I realize there's a bug here: https://github.com/djkazic/aperture/blob/4e3194e169b577cc63b887331415a533043bab39/l402/header.go#L58-L69

That should be a break.

Comment thread l402/header.go
return nil, lntypes.Preimage{}, nil, fmt.Errorf("unable to "+
"unmarshal macaroon: %v", err)
}
if len(slice) == 0 {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add a test case for this.

@djkazic
Copy link
Copy Markdown
Contributor Author

djkazic commented Apr 23, 2026

Will address review comments in the AM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants