mirror of
https://github.com/zap-proto/js.git
synced 2026-10-30 15:04:48 +00:00
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.
98 lines
2.5 KiB
Plaintext
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[:]))
|
|
}
|