Skip to content

Expired biscuits are accepted on the node dataplane: SamNode.Authorize does not inject the time fact / expiration check #296

Description

@sachouri611

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 <= $expapi/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

  1. Mint a biscuit via the control plane (the bootstrap flow is sufficient) with a short expiration.
  2. Wait past the expiration.
  3. identity.VerifyBiscuit rejects it (time check fires).
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions