Version affected: main @ cdaf83e19bb5d9f6c90cb69abd341093aef1f4d2
Summary
There appear to be two biscuit verification paths with different guarantees:
- The generic verifier
identity.VerifyBiscuit / VerifyBiscuitAndGetKey injects the time(now) fact and enforces ControlPlaneStaticTimeCheck (check if time($time), expiration($exp), $time <= $exp — api/datalog.go:357).
- The node dataplane authorizer
SamNode.Authorize (internal/node/middleware.go) builds its authorizer without adding the time(...) fact and without the static time check. It injects FactService and FactConnectionPeerID and adds BaselineReplayCheck, but grep -n "FactTime\|ControlPlaneStaticTimeCheck" internal/node/middleware.go returns no match.
Since biscuit expiration is a Datalog check over a time fact, no fact means no check: an expired biscuit that still parses and whose signature chain verifies is accepted on the tool-invocation path, while the generic verifier rejects the same token.
Reproduction sketch
- Mint a biscuit via the control plane (the bootstrap flow is sufficient) with a short
expiration.
- Wait past the expiration.
identity.VerifyBiscuit rejects it (time check fires).
- A node-side authorized call (MCP stream handshake, or HTTP-over-libp2p ingress) accepts the same token.
Expected behavior
Every authorization path that consults biscuit expiry enforces it — or expiry enforcement is centralized in one shared helper so the paths cannot diverge.
Questions / notes
- Is the omission intentional (e.g. does the dataplane rely on some other freshness mechanism we missed)? If so, a comment at the call site would help readers.
- Possibly related, and happy to file separately if confirmed:
NewServer installs NewNopMeshAdapter() (internal/controlplane/server.go), whose PublishEvent does not publish (internal/controlplane/mesh.go), and cmd/sam-control-plane/main.go never replaces it. Rotation/revocation events look implemented and handled node-side, but not propagated by the shipped control-plane binary. The tests that exercise propagation call SetMeshAdapter themselves.
Environment
Studied from source on Windows. The behavior looks platform-independent, since it stems from fact injection that is absent regardless of OS.
Thanks for the project — the identity model (libp2p peer ID + attenuable Biscuit + OIDC provenance) is a genuinely nice design, which is why the asymmetry above stood out.
Version affected:
main@cdaf83e19bb5d9f6c90cb69abd341093aef1f4d2Summary
There appear to be two biscuit verification paths with different guarantees:
identity.VerifyBiscuit/VerifyBiscuitAndGetKeyinjects thetime(now)fact and enforcesControlPlaneStaticTimeCheck(check if time($time), expiration($exp), $time <= $exp—api/datalog.go:357).SamNode.Authorize(internal/node/middleware.go) builds its authorizer without adding thetime(...)fact and without the static time check. It injectsFactServiceandFactConnectionPeerIDand addsBaselineReplayCheck, butgrep -n "FactTime\|ControlPlaneStaticTimeCheck" internal/node/middleware.goreturns no match.Since biscuit expiration is a Datalog check over a time fact, no fact means no check: an expired biscuit that still parses and whose signature chain verifies is accepted on the tool-invocation path, while the generic verifier rejects the same token.
Reproduction sketch
expiration.identity.VerifyBiscuitrejects it (time check fires).Expected behavior
Every authorization path that consults biscuit expiry enforces it — or expiry enforcement is centralized in one shared helper so the paths cannot diverge.
Questions / notes
NewServerinstallsNewNopMeshAdapter()(internal/controlplane/server.go), whosePublishEventdoes not publish (internal/controlplane/mesh.go), andcmd/sam-control-plane/main.gonever replaces it. Rotation/revocation events look implemented and handled node-side, but not propagated by the shipped control-plane binary. The tests that exercise propagation callSetMeshAdapterthemselves.Environment
Studied from source on Windows. The behavior looks platform-independent, since it stems from fact injection that is absent regardless of OS.
Thanks for the project — the identity model (libp2p peer ID + attenuable Biscuit + OIDC provenance) is a genuinely nice design, which is why the asymmetry above stood out.