Files
js/test/cap_go_kat_gen.go.txt
zeekay c86c75efb0 feat(cap): real capability lifecycle — issue/attenuate/verify/verifyChain/revoke
Port the ZAP capability layer to TypeScript at src/cap.ts, the byte-for-byte
peer of github.com/zap-proto/go/cap (SPEC.md §2.3/§3/§4 + capabilities_kinds.md).
Exported at @zap-proto/zap/cap; the universal root stays browser-safe.

- Cap view/builder over the zero-copy runtime for capabilities.zap (v1.1, 3408B
  Sig footer); Capability/Caveat/Revocation codec vendored in, as Go vendors
  capabilities_zap.go into package cap.
- canonicalBytes: Capability[0..164) || canonical(Caveats) — byte-identical to
  Go's signed scope; heap-indirection excluded so it is identical across runtimes.
- issue + attenuate: mint-time delegation gate (parent PermAttenuate or
  Kind==Delegate), monotonic perm narrowing, holder→issuer linkage, expiry
  shrink-only. Signs canonicalBytes.
- verify + verifyChain: all SPEC §2.3 invariants (monotonic perms, holder
  linkage, target invariance, root zero-Parent, delegation gate) + FAIL-CLOSED
  scheme dispatch (reject 0x00/unknown; never downgrade; never return ok on an
  unverified sig).
- capId = SHA-256(canonicalBytes || Sig) (spec-corrected from BLAKE3).
- Schemes: Ed25519 MANDATORY via node:crypto (synchronous, RFC 8032, raw seed
  framed into PKCS8/SPKI). ML-DSA-65 / hybrid / secp256k1 fail-closed unless a
  schemeVerify hook is wired — the JS runtime fabricates no PQ verify.
- Zero npm runtime deps preserved (node:crypto is stdlib); synchronous API
  matching Go exactly, no async contagion through the auth primitive.

Cross-language KAT (interop proof): test/cap_go_kat.hex is a Go-signed cap over
a fixed Ed25519 seed (32x0x42); test/cap_kat.test.ts decodes it, asserts JS
canonicalBytes + capId match Go's, verifies the Go Ed25519 sig in JS -> true,
and a tampered variant -> false. Re-issuing the same inputs+seed in JS
reproduces Go's exact CapID (1b809edc...), signature, and full wire bytes.

Tests: 110 pass (66 pre-existing + 44 new mirroring Go's 34 cap tests:
happy/attenuate-refuses/chain-rejects/fail-closed/revocation + KAT + custom
Signer). Bump @zap-proto/zap 1.4.2 -> 1.5.0.
2026-06-21 01:15:57 -07:00

98 lines
2.5 KiB
Plaintext

package main
import (
"crypto/ed25519"
"encoding/binary"
"encoding/hex"
"fmt"
zcap "github.com/zap-proto/go/cap"
)
// fixedSigner is an ed25519 signer over a FIXED 32-byte seed so the KAT is
// fully deterministic and reproducible from JS. It mirrors cap.Ed25519Signer
// (64-byte sig at the leading bytes, SchemeEd25519 tag at AlgTagOffset) but
// pins the key material instead of generating it.
type fixedSigner struct {
priv ed25519.PrivateKey
pub [32]byte
}
func newFixedSigner(seed []byte) *fixedSigner {
priv := ed25519.NewKeyFromSeed(seed)
s := &fixedSigner{priv: priv}
s.pub = zcap.Hash32(priv.Public().(ed25519.PublicKey))
return s
}
func (s *fixedSigner) Sign(payload []byte) ([zcap.SigSize]byte, error) {
var out [zcap.SigSize]byte
sig := ed25519.Sign(s.priv, payload)
copy(out[:ed25519.SignatureSize], sig)
out[zcap.AlgTagOffset] = byte(zcap.SchemeEd25519)
return out, nil
}
func (s *fixedSigner) Public() [32]byte { return s.pub }
func u64bytes(v uint64) []byte {
out := make([]byte, 8)
binary.LittleEndian.PutUint64(out, v)
return out
}
func u32pair(a, b uint32) []byte {
out := make([]byte, 8)
binary.LittleEndian.PutUint32(out[0:4], a)
binary.LittleEndian.PutUint32(out[4:8], b)
return out
}
func main() {
seed := make([]byte, 32)
for i := range seed {
seed[i] = 0x42
}
signer := newFixedSigner(seed)
var target, holder [32]byte
for i := range target {
target[i] = byte(i) // 0x00..0x1f
holder[i] = byte(0xff - i) // 0xff..0xe0
}
in := zcap.Issuance{
Kind: uint32(zcap.KindIAMSession),
Target: target,
Holder: holder,
Permissions: zcap.PermAttenuate | 0x000000000000000F,
// Parent stays zero -> root cap.
IssuedAt: 1700000000,
ExpiresAt: 1900000000,
Caveats: []zcap.Caveat{
{Kind: zcap.CaveatMaxAmount, Value: u64bytes(1_000_000)},
{Kind: zcap.CaveatRateLimit, Value: u32pair(60, 10)},
{Kind: zcap.CaveatIPCIDR, Value: []byte("10.0.0.0/8")},
},
}
c, err := zcap.Issue(in, signer)
if err != nil {
panic(err)
}
id := c.ID()
canon := c.CanonicalBytes()
sig := c.Signature()
rawPub := signer.priv.Public().(ed25519.PublicKey)
// Emit a tiny KEY=hex manifest the JS KAT test parses.
fmt.Printf("seed=%s\n", hex.EncodeToString(seed))
fmt.Printf("pubkey=%s\n", hex.EncodeToString(rawPub))
fmt.Printf("issuer=%s\n", hex.EncodeToString(signer.pub[:]))
fmt.Printf("cap=%s\n", hex.EncodeToString(c.Bytes()))
fmt.Printf("canonical=%s\n", hex.EncodeToString(canon))
fmt.Printf("capid=%s\n", hex.EncodeToString(id[:]))
fmt.Printf("sig=%s\n", hex.EncodeToString(sig[:]))
}