Files
go/cap/spec_test.go
zeekay a1bf2bfa1e feat: RPC/interfaces codegen + cap spec-conformance (SPEC §2.3/§3) + rebrand finish
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.
2026-06-20 00:02:22 -07:00

292 lines
10 KiB
Go

// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package cap
import (
"bytes"
"encoding/binary"
"errors"
"testing"
)
// ----------------------------------------------------------------------------
// B2 — VerifyChain delegation gate (SPEC §2.3 step 3d) and mint-time refusal.
// ----------------------------------------------------------------------------
// TestAttenuateRefusesWithoutPermAttenuate proves a parent that lacks
// PermAttenuate and is not a CapKindDelegate cannot mint a child, even by
// its own holder — SPEC §7 (the runtime refuses to build a cap its own
// verifier would reject).
func TestAttenuateRefusesWithoutPermAttenuate(t *testing.T) {
root := mustSigner(t)
holder := mustSigner(t)
parent, err := Issue(Issuance{
Kind: uint32(KindIAMSession),
Holder: root.Public(),
Permissions: 0xFF, // no PermAttenuate, not CapKindDelegate
ExpiresAt: 2000000000,
}, root)
if err != nil {
t.Fatal(err)
}
if _, err := Attenuate(parent, holder.Public(), 0x0F, nil, 0, root); !errors.Is(err, ErrNotDelegable) {
t.Errorf("expected ErrNotDelegable, got %v", err)
}
}
// TestAttenuateAllowedForDelegateKind proves a CapKindDelegate parent may
// mint a child WITHOUT carrying PermAttenuate (the kind itself authorizes
// delegation, per capabilities_kinds.md).
func TestAttenuateAllowedForDelegateKind(t *testing.T) {
root := mustSigner(t)
holder := mustSigner(t)
parent, err := Issue(Issuance{
Kind: uint32(KindDelegate),
Holder: root.Public(),
Permissions: 0xFF, // no PermAttenuate, but Kind == Delegate
ExpiresAt: 2000000000,
}, root)
if err != nil {
t.Fatal(err)
}
if _, err := Attenuate(parent, holder.Public(), 0x0F, nil, 0, root); err != nil {
t.Errorf("delegate-kind parent should permit attenuation: %v", err)
}
}
// TestVerifyChainRejectsUndelegatedParent constructs (out of band, via a
// permissive signer that lets us forge a chain the mint-time gate would
// block) a leaf whose parent lacks PermAttenuate, and proves VerifyChain
// independently enforces the gate at verify time — defense in depth.
func TestVerifyChainRejectsUndelegatedParent(t *testing.T) {
root := mustSigner(t)
mid := mustSigner(t)
leaf := mustSigner(t)
var target [32]byte
target[0] = 0x7E
// Root WITHOUT PermAttenuate. Mint mid by issuing directly (bypassing
// Attenuate's gate) but with a correct chain shape: mid.Issuer = root,
// mid.Parent = rootCap.ID().
rootCap, err := Issue(Issuance{
Kind: uint32(KindIAMSession), Holder: root.Public(), Target: target,
Permissions: 0x0F, ExpiresAt: 2000000000,
}, root)
if err != nil {
t.Fatal(err)
}
// Issue derives Issuer from the signer (root), so mid.Issuer = root.
midCap, err := Issue(Issuance{
Kind: uint32(KindIAMSession), Holder: mid.Public(),
Target: target, Permissions: 0x07, Parent: rootCap.ID(), ExpiresAt: 2000000000,
}, root)
if err != nil {
t.Fatal(err)
}
_ = leaf
v := Verifier{IssuerKey: issuerKeyFn(root, mid)}
// Verify midCap against rootCap as its parent: rootCap lacks
// PermAttenuate, so the chain must be refused.
chain := []Cap{rootCap}
if err := v.VerifyChain(midCap, chain, 0x01, target, mid.Public(), 1700000000); !errors.Is(err, ErrNotDelegable) {
t.Errorf("expected ErrNotDelegable from undelegated parent, got %v", err)
}
}
// Issue with an explicit non-root Parent/Issuer needs the Issuance to
// carry Issuer; the public Issue derives Issuer from the signer, so we
// confirm that path here is consistent with the chain shape above.
// ----------------------------------------------------------------------------
// B3 — signature scope is SPEC §3 canonical bytes, not the whole buffer.
// ----------------------------------------------------------------------------
// TestCanonicalBytesShape pins the exact layout of the signed bytes:
// Capability[0..164) || (Kind:u32-LE || len:u32-LE || Value) per caveat.
func TestCanonicalBytesShape(t *testing.T) {
signer := mustSigner(t)
caveats := []Caveat{
{Kind: CaveatMaxAmount, Value: u64bytes(7)},
{Kind: CaveatIPCIDR, Value: []byte("10.0.0.0/8")},
}
c, err := Issue(Issuance{
Kind: uint32(KindKMSAccess), Permissions: 0xFF, ExpiresAt: 2000000000, Caveats: caveats,
}, signer)
if err != nil {
t.Fatal(err)
}
got := c.CanonicalBytes()
// Reconstruct the expected bytes independently.
root := capRootOff(c.Bytes())
want := append([]byte(nil), c.Bytes()[root:root+signedHeaderLen]...)
for _, cv := range caveats {
var hdr [8]byte
binary.LittleEndian.PutUint32(hdr[0:4], uint32(cv.Kind))
binary.LittleEndian.PutUint32(hdr[4:8], uint32(len(cv.Value)))
want = append(want, hdr[:]...)
want = append(want, cv.Value...)
}
if !bytes.Equal(got, want) {
t.Errorf("CanonicalBytes mismatch\n got=%x\nwant=%x", got, want)
}
// The signed length is the header (164) plus each caveat's 8-byte
// prefix and value.
wantLen := signedHeaderLen + (8 + 8) + (8 + len("10.0.0.0/8"))
if len(got) != wantLen {
t.Errorf("CanonicalBytes len = %d, want %d", len(got), wantLen)
}
// It must NOT include the Sig field (3408 bytes at offset 164).
if len(got) >= signedHeaderLen+SigSize {
t.Errorf("CanonicalBytes appears to include Sig footer (len %d)", len(got))
}
}
// TestSignatureExcludesSigField proves that mutating bytes in the Sig
// footer (other than via a real re-sign) does not change the canonical
// signed bytes — the signature is computed over [0..164)+caveats only, so
// the verifier still accepts after a footer-pad scribble that leaves the
// algorithm tag intact.
func TestSignatureExcludesSigField(t *testing.T) {
signer := mustSigner(t)
c, err := Issue(Issuance{Kind: uint32(KindKMSAccess), Permissions: 0xFF, ExpiresAt: 2000000000}, signer)
if err != nil {
t.Fatal(err)
}
before := append([]byte(nil), c.CanonicalBytes()...)
// Scribble a zero-pad byte inside the Sig footer (after the 64-byte
// ed25519 signature, before the tag) in a copy of the buffer.
raw := append([]byte(nil), c.Bytes()...)
sigOff := sigAbsOff(raw)
raw[sigOff+100] ^= 0xFF // a pad byte, not the real signature or tag
c2, err := Wrap(raw)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(before, c2.CanonicalBytes()) {
t.Errorf("CanonicalBytes changed when only Sig pad was scribbled")
}
}
// TestVerifyDetectsHeaderTamper proves tampering with a byte INSIDE the
// signed header (e.g. Permissions) breaks verification — the canonical
// bytes do cover the fixed header.
func TestVerifyDetectsHeaderTamper(t *testing.T) {
signer := mustSigner(t)
c, err := Issue(Issuance{Kind: uint32(KindKMSAccess), Permissions: 0xFF, ExpiresAt: 2000000000}, signer)
if err != nil {
t.Fatal(err)
}
raw := append([]byte(nil), c.Bytes()...)
raw[fieldAbsOff(raw, capabilityViewPermissionsOff)] ^= 0x01
tc, err := Wrap(raw)
if err != nil {
t.Fatal(err)
}
v := Verifier{IssuerKey: issuerKeyFn(signer)}
if err := v.Verify(tc, 1); !errors.Is(err, ErrSigMismatch) {
t.Errorf("expected ErrSigMismatch on header tamper, got %v", err)
}
}
// ----------------------------------------------------------------------------
// B4 — VerifyRevocation is scheme-aware (dispatches on the tag byte).
// ----------------------------------------------------------------------------
// TestVerifyRevocationSchemeAware proves the package VerifyRevocation
// accepts a (bootstrap ed25519) revocation by routing on its tag byte,
// and that Verifier.VerifyRevocation can dispatch a non-ed25519 scheme to
// a wired SchemeVerify hook.
func TestVerifyRevocationSchemeAware(t *testing.T) {
signer := mustSigner(t)
c, _ := Issue(Issuance{Permissions: 1, ExpiresAt: 2000000000}, signer)
r, err := Revoke(c, 100, signer)
if err != nil {
t.Fatal(err)
}
// Bootstrap path: tag is SchemeEd25519, accepted by the package func.
if err := VerifyRevocation(r, signer.PublicKey()); err != nil {
t.Errorf("ed25519 revocation rejected: %v", err)
}
// A revocation carrying a non-ed25519 tag must be routed to the hook.
rPQ := r
rPQ.RevokerSig[AlgTagOffset] = byte(SchemeMLDSA65)
var sawScheme Scheme
v := Verifier{
SchemeVerify: func(s Scheme, pub, payload []byte, sig [SigSize]byte) error {
sawScheme = s
if s == SchemeMLDSA65 {
return nil // pretend the PQ signature verifies
}
return ErrUnhandledScheme
},
}
if err := v.VerifyRevocation(rPQ, signer.PublicKey()); err != nil {
t.Errorf("hooked ML-DSA-65 revocation rejected: %v", err)
}
if sawScheme != SchemeMLDSA65 {
t.Errorf("SchemeVerify saw %#x, want SchemeMLDSA65", sawScheme)
}
}
// TestVerifyRevocationFailsClosed proves a revocation whose tag is
// SchemeReserved (0x00) or an unknown value is rejected, never silently
// accepted via an ed25519 fallback (B6 applied to revocations).
func TestVerifyRevocationFailsClosed(t *testing.T) {
signer := mustSigner(t)
c, _ := Issue(Issuance{Permissions: 1, ExpiresAt: 2000000000}, signer)
r, _ := Revoke(c, 100, signer)
for _, tag := range []byte{0x00, 0x7F, 0xFF} {
bad := r
bad.RevokerSig[AlgTagOffset] = tag
if err := VerifyRevocation(bad, signer.PublicKey()); !errors.Is(err, ErrUnhandledScheme) {
t.Errorf("tag %#x: expected ErrUnhandledScheme, got %v", tag, err)
}
}
}
// ----------------------------------------------------------------------------
// B6 — verifySig fails closed on scheme==0 / unknown (cap signatures).
// ----------------------------------------------------------------------------
// TestVerifyFailsClosedOnUnknownScheme proves Verify refuses a cap whose
// Sig algorithm tag is SchemeReserved (0x00) or otherwise unimplemented,
// rather than falling back to ed25519.
func TestVerifyFailsClosedOnUnknownScheme(t *testing.T) {
signer := mustSigner(t)
c, _ := Issue(Issuance{Permissions: 1, ExpiresAt: 2000000000}, signer)
v := Verifier{IssuerKey: issuerKeyFn(signer)}
for _, tag := range []byte{0x00, 0x7F, 0xFF} {
raw := append([]byte(nil), c.Bytes()...)
raw[sigAbsOff(raw)+AlgTagOffset] = tag
bad, err := Wrap(raw)
if err != nil {
t.Fatal(err)
}
if err := v.Verify(bad, 1); !errors.Is(err, ErrUnhandledScheme) {
t.Errorf("tag %#x: expected ErrUnhandledScheme, got %v", tag, err)
}
}
}
// TestSchemeKnownSet pins exactly which scheme tags are accepted.
func TestSchemeKnownSet(t *testing.T) {
known := map[Scheme]bool{
SchemeSecp256k1: true, SchemeEd25519: true, SchemeMLDSA65: true, SchemeHybrid: true,
}
for s := 0; s <= 0xFF; s++ {
want := known[Scheme(s)]
if Scheme(s).known() != want {
t.Errorf("Scheme(%#x).known() = %v, want %v", s, Scheme(s).known(), want)
}
}
if SchemeReserved.known() {
t.Errorf("SchemeReserved must not be known")
}
}