mirror of
https://github.com/zap-proto/go.git
synced 2026-10-30 15:05:00 +00:00
zapgen — interface (RPC) support: - Parse `interface` decls (brace + whitespace forms, one parser) with 1-based auto-assigned method ordinals; methods take ≤1 struct request and (after `returns`) ≤1 struct response, matching the spec README grammar. - desugar: `interface` opens a whitespace block; method lines pass through verbatim (no @N). Block headers now honored only at file scope, so a struct field NAMED `interface`/`struct` stays a field (H4 preserved). - emit: per interface a 1-based ordinal table, a typed Client over a Channel seam, and an abstract Handler + Dispatch<Iface> ordinal router, all over the new runtime call envelope. Method names exported. - New `rpc` package: the ZAP call envelope (BuildRequest/ParseRequest, BuildResponse/ParseResponse, Call, Response, status codes), byte-compatible with the other runtimes' transport envelopes (Version2 header + router msgType flags). Adds zap.NewBuilderV2. - examples/echo: generated service + end-to-end client↔dispatch↔handler test covering request/response, void, no-request, bare, handler-error, and unknown-ordinal paths. Golden fixtures for the interface codegen. cap — reconcile to SPEC v1.1: - B3 signature scope: sign SPEC §3 canonical bytes (Capability[0..164) || canonical(Caveats), Kind:u32||len:u32||Value), via Cap.CanonicalBytes — one definition shared by signer and verifier; excludes Sig and heap indirection so caps verify cross-language. CapID = Hash32(Canonical||Sig). - B2 delegation gate: define cross-cutting Permission bits (PermAttenuate 1<<32, PermAudit, PermRoot); enforce SPEC §2.3 step 3d in VerifyChain and at mint time in Attenuate (refuse a cap the verifier would reject). - B4/B6: VerifyRevocation is scheme-aware (dispatches on the tag byte); verifySig fails closed on SchemeReserved/unknown instead of falling back to ed25519. Verifier.VerifyRevocation added for PQ revocations. - spec_test.go covers all four. Rebrand finish + docs: - Zero capnp/Cap'n Proto and zero hanzo/avalabs/capnproto.org tokens in zap-native source (zap.go doc neutralized; cap doc-comment brand refs generalized). LLM.md refreshed: zapgen shipped (not "forthcoming"), rpc package, interface DSL, cap signature scope. Ignore the agents example binary. go build ./... and go test ./... -count=1 pass; race-clean on changed pkgs.
158 lines
5.3 KiB
Go
158 lines
5.3 KiB
Go
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package cap
|
|
|
|
import (
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// Issuance describes the request to mint a new root capability.
|
|
type Issuance struct {
|
|
Kind uint32
|
|
Target [32]byte
|
|
Holder [32]byte
|
|
Permissions uint64
|
|
Parent [32]byte // zero = root
|
|
IssuedAt int64 // 0 = use time.Now()
|
|
ExpiresAt int64 // 0 = no expiry
|
|
Caveats []Caveat
|
|
}
|
|
|
|
// errMissingSigner is returned when Issue/Attenuate are called without a
|
|
// concrete Signer. We don't ship a default — every cap must be signed.
|
|
var errMissingSigner = errors.New("cap: signer required")
|
|
|
|
// Issue mints a new root capability signed by signer. The signer's
|
|
// Public() becomes the cap's Issuer field. Parent stays as supplied
|
|
// (zero for a true root; non-zero for re-issuing under an existing
|
|
// parent at the cost of caller asserting the chain).
|
|
//
|
|
// To derive a child cap from an existing parent, use Attenuate.
|
|
func Issue(in Issuance, signer Signer) (Cap, error) {
|
|
if signer == nil {
|
|
return Cap{}, errMissingSigner
|
|
}
|
|
if in.IssuedAt == 0 {
|
|
in.IssuedAt = time.Now().Unix()
|
|
}
|
|
issuer := signer.Public()
|
|
raw, err := buildCapBytes(in, issuer, signer)
|
|
if err != nil {
|
|
return Cap{}, err
|
|
}
|
|
return Wrap(raw)
|
|
}
|
|
|
|
// Attenuate derives a child cap from parent by intersecting permissions
|
|
// and adding caveats. The child's Issuer = parent's Holder; signer must
|
|
// hold the parent's holder key (this is the basis for chain validation:
|
|
// each link is signed by the previous holder's key).
|
|
//
|
|
// The child target equals the parent's target — attenuation never
|
|
// broadens scope. permissions is intersected with the parent's
|
|
// permissions; passing 0xFFFF... is equivalent to "inherit all".
|
|
// caveats are appended (not replaced); chain validation evaluates them
|
|
// in conjunction with the parent's.
|
|
//
|
|
// expiresAt of 0 inherits the parent's expiry; non-zero overrides
|
|
// downward (the child cannot outlive the parent).
|
|
func Attenuate(parent Cap, holder [32]byte, permissions uint64, caveats []Caveat,
|
|
expiresAt int64, signer Signer) (Cap, error) {
|
|
if signer == nil {
|
|
return Cap{}, errMissingSigner
|
|
}
|
|
if signer.Public() != parent.Holder() {
|
|
// The signer must be the parent's holder; only the holder can
|
|
// delegate authority downward. Anything else would let a
|
|
// stranger mint children, defeating the chain's whole purpose.
|
|
return Cap{}, ErrChainBroken
|
|
}
|
|
// SPEC §7: refuse to build a cap that would fail its own verifier.
|
|
// The delegation gate (SPEC §2.3 step 3d) requires the parent to
|
|
// carry PermAttenuate or be a CapKindDelegate cap; mint-time
|
|
// enforcement means a child that VerifyChain would reject is never
|
|
// produced in the first place.
|
|
if parent.Permissions()&PermAttenuate == 0 && CapKind(parent.Kind()) != KindDelegate {
|
|
return Cap{}, ErrNotDelegable
|
|
}
|
|
parentExpiry := parent.ExpiresAt()
|
|
switch {
|
|
case expiresAt == 0:
|
|
expiresAt = int64(parentExpiry)
|
|
case parentExpiry != 0 && uint64(expiresAt) > parentExpiry:
|
|
expiresAt = int64(parentExpiry)
|
|
}
|
|
parentID := parent.ID()
|
|
in := Issuance{
|
|
Kind: parent.Kind(),
|
|
Target: parent.Target(),
|
|
Holder: holder,
|
|
Permissions: permissions & parent.Permissions(),
|
|
Parent: parentID,
|
|
IssuedAt: time.Now().Unix(),
|
|
ExpiresAt: expiresAt,
|
|
Caveats: caveats,
|
|
}
|
|
issuer := signer.Public()
|
|
raw, err := buildCapBytes(in, issuer, signer)
|
|
if err != nil {
|
|
return Cap{}, err
|
|
}
|
|
return Wrap(raw)
|
|
}
|
|
|
|
// buildCapBytes serializes a capability into the canonical ZAP wire
|
|
// format and signs it. The signed payload is SPEC §3 canonical bytes
|
|
// (Capability[0..164) || canonical(Caveats)), computed via
|
|
// Cap.CanonicalBytes so signer and verifier share one definition. After
|
|
// signing, the SigSize-byte signature is patched into the Sig field
|
|
// in-place. Returns the final wire bytes.
|
|
func buildCapBytes(in Issuance, issuer [32]byte, signer Signer) ([]byte, error) {
|
|
// Build each Caveat as its own ZAP-framed sub-message; the canonical
|
|
// list element is the full ZAP buffer length-prefixed by ListBuilder
|
|
// .AddObjectBytes.
|
|
caveatBufs := make([][]byte, len(in.Caveats))
|
|
for i, cv := range in.Caveats {
|
|
caveatBufs[i] = NewCaveatView(CaveatViewInput{
|
|
Kind: uint32(cv.Kind),
|
|
Value: cv.Value,
|
|
})
|
|
}
|
|
|
|
// First pass: build with Sig = zero. The Sig field's bytes are NOT in
|
|
// the signing scope (CanonicalBytes covers [0..164) + caveats only),
|
|
// so the zero placeholder does not affect the signature.
|
|
raw := NewCapabilityView(CapabilityViewInput{
|
|
Kind: in.Kind,
|
|
Target: in.Target,
|
|
Holder: in.Holder,
|
|
Issuer: issuer,
|
|
Permissions: in.Permissions,
|
|
Parent: in.Parent,
|
|
IssuedAt: uint64(in.IssuedAt),
|
|
ExpiresAt: uint64(in.ExpiresAt),
|
|
Caveats: caveatBufs,
|
|
// Sig left as the zero [SigSize]byte.
|
|
})
|
|
|
|
// Compute the canonical signing bytes from the just-built buffer using
|
|
// the same code path the verifier uses — no asymmetry between build
|
|
// and verify (SPEC §7).
|
|
c, err := Wrap(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sig, err := signer.Sign(c.CanonicalBytes())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Patch the sig field in-place. The Sig field sits at
|
|
// rootOff + capabilityViewSigOff in the final buffer.
|
|
sigOff := sigAbsOff(raw)
|
|
copy(raw[sigOff:sigOff+SigSize], sig[:])
|
|
return raw, nil
|
|
}
|