Documentation
¶
Overview ¶
Package identity implements the Contract-B trust-root signing core: RFC 8785 (JCS) canonicalization, ed25519 signing/verification over the canonical bytes, and the Contract-B schema-validation gate.
The cardinal rule of this package is that signatures are ALWAYS computed over canonical bytes. To make that rule type-enforced, canonical bytes carry their own type (CanonicalBytes): SignCanonical/VerifyCanonical accept ONLY CanonicalBytes, so a caller cannot pass raw JSON into the low-level signing path — that is a COMPILE error, not a runtime footgun.
Callers should prefer SignEntry/VerifyEntry: these are the VALIDATED entry points — they run the Contract-B schema gate (ValidateSchema) AND canonicalize before signing/verifying, so a caller cannot accidentally sign a non-conformant or non-canonical entry.
SignCanonical/VerifyCanonical are the low-level primitives. They BYPASS the schema gate and assume the caller already holds JCS-canonical bytes (e.g. a registry/replay path verifying a frozen vector). Do not use them on untrusted raw JSON — prefer SignEntry/VerifyEntry there.
Verification is COFACTORLESS strict verification, matching the Rust verifier on the other side of this trust root (ed25519-dalek verify_strict). The two verifiers MUST reach the same verdict on identical trust-root bytes; any divergence is a forgeable seam. ZIP-215 (cofactored) deliberately ACCEPTS small-order / mixed-order R components for blockchain-consensus determinism, whereas verify_strict REJECTS them — so verify_strict is the ruleset that keeps Go and Rust in lockstep. The strict check rejects, as STRUCTURAL failures:
- A small-order (or undecodable) public key A. The forgery {A=0, R=0, S=0} satisfies [S]B = R + [k]A for EVERY message, so any small-order pubkey is a universal-forgery vector. A point is small-order iff [8]A is the identity (8 is the edwards25519 cofactor).
- A small-order (or undecodable) signature R component. ZIP-215 would accept these; verify_strict (and so this code) rejects them.
- A non-canonical S scalar (S >= L, the group order).
The signature is then confirmed cofactorlessly via [S]B - [k]A == R.
Signing stays on stdlib crypto/ed25519 (it emits canonical RFC-8032 signatures, so the frozen vector still verifies under strict verification).
TODO(contract-b registry slice): introduce a SignedEntry domain type bundling {entry, pubkey, sig} and a validating PublicKey constructor; both are deferred to the registry slice so this slice stays the signing/validation core only.
Index ¶
- Constants
- func SignCanonical(priv ed25519.PrivateKey, canonical CanonicalBytes) ([]byte, error)
- func SignEntry(priv ed25519.PrivateKey, jsonBytes []byte) ([]byte, error)
- func ValidateSchema(jsonBytes []byte) error
- func VerifyCanonical(pub ed25519.PublicKey, canonical CanonicalBytes, sig []byte) (bool, error)
- func VerifyEntry(pub ed25519.PublicKey, jsonBytes []byte, sig []byte) (bool, error)
- type CanonicalBytes
Constants ¶
const ( // ErrVerifyBadPubKeyLen is returned for a public key that is not // ed25519.PublicKeySize bytes. ErrVerifyBadPubKeyLen = "identity: verify: public key must be ed25519.PublicKeySize bytes" // ErrVerifyBadSigLen is returned for a signature that is not // ed25519.SignatureSize bytes. ErrVerifyBadSigLen = "identity: verify: signature must be ed25519.SignatureSize bytes" // ErrVerifySmallOrderPubKey is returned for a small-order (universal-forgery) // or otherwise undecodable public key. ErrVerifySmallOrderPubKey = "identity: verify: public key is small-order or undecodable" // ErrVerifySmallOrderR is returned for a signature whose R component is // small-order or undecodable. ZIP-215 would accept these; cofactorless // strict verification (matching dalek verify_strict) rejects them. ErrVerifySmallOrderR = "identity: verify: signature R is small-order or undecodable" // ErrVerifyNonCanonicalR is returned for a signature whose R component is a // non-canonical point encoding (y >= p). filippo SetBytes silently reduces it // mod p, but dalek verify_strict compares the COMPRESSED bytes of the // recomputed R against sig[:32] — so a non-canonical R it rejects must be // rejected here too, or Go would accept what Rust rejects (a forgeable seam). ErrVerifyNonCanonicalR = "identity: verify: signature R is a non-canonical encoding (y >= p)" // ErrVerifyNonCanonicalS is returned for a signature whose S scalar is // non-canonical (S >= L, the group order). ErrVerifyNonCanonicalS = "identity: verify: signature S is non-canonical (>= L)" )
Structural verification-reject messages (SSOT). VerifyCanonical returns these as a non-nil error so an operator can distinguish a malformed-input / forgery-key rejection from an honest signature non-match (which is (false, nil)). They are EXPORTED so callers/tests reference the single definition.
const ( // ErrSchemaFloatNumber is the message for a non-integer (float) number. ErrSchemaFloatNumber = "identity: schema: non-integer number not allowed" // ErrSchemaIntTooLarge is the message for an integer whose magnitude exceeds 2^53. ErrSchemaIntTooLarge = "identity: schema: integer exceeds 2^53" // ErrSchemaNonASCIIKey is the message for a non-ASCII object key. ErrSchemaNonASCIIKey = "identity: schema: object key must be ASCII" // ErrSchemaDuplicateKey is the message for a duplicate object key. ErrSchemaDuplicateKey = "identity: schema: duplicate object key" // ErrSchemaTrailingInput is the message for trailing data after the JSON value. ErrSchemaTrailingInput = "identity: schema: trailing data after JSON value" // ErrSchemaNotNFC is the message for a string (value or key) that is not Unicode NFC. ErrSchemaNotNFC = "identity: schema: string must be Unicode NFC-normalized" // ErrSchemaNotObject is the message for a top-level document that is not a // JSON object. Contract-B entries are objects; a bare scalar/array is rejected. ErrSchemaNotObject = "identity: schema: top-level value must be a JSON object" )
Validation error messages (SSOT — referenced from the walk, not inlined). The messages tests assert against are EXPORTED so callers/tests reference the single definition (no duplicated literals) when distinguishing which rule fired; purely-internal messages stay unexported.
Variables ¶
This section is empty.
Functions ¶
func SignCanonical ¶
func SignCanonical(priv ed25519.PrivateKey, canonical CanonicalBytes) ([]byte, error)
SignCanonical produces an ed25519 signature over the already-canonical bytes. It BYPASSES the Contract-B schema gate, so callers must already hold JCS-canonical, schema-valid bytes (registry/replay use only) — prefer SignEntry for raw JSON. It returns an error (never panics) on a nil or wrong-length private key. The CanonicalBytes parameter type makes it impossible to pass raw JSON into this path.
func SignEntry ¶
func SignEntry(priv ed25519.PrivateKey, jsonBytes []byte) ([]byte, error)
SignEntry is a VALIDATED entry point: it runs the Contract-B schema gate, canonicalizes the raw JSON entry, then signs the canonical bytes. It makes it impossible to sign a non-conformant or non-canonical entry. It returns an error for a schema violation, uncanonicalizable JSON, or a nil/wrong-length private key (never panics). The (sig, err) it returns is SignCanonical's, returned directly after the entry has been validated and canonicalized.
func ValidateSchema ¶
ValidateSchema is the Contract-B validation gate. Contract-B entries are JSON OBJECTS, so it first rejects any top-level value that is not an object, then walks the entire document and rejects anything that would break cross-language, JCS-safe signing:
- a top-level value that is not a JSON object (bare scalar or array),
- non-integer numbers (floats such as 1.0 or 1e3),
- integers strictly greater than 2^53,
- non-ASCII object keys (this rule also subsumes NFC for keys, since every non-NFC codepoint is non-ASCII),
- string values that are not valid UTF-8,
- string values that are not Unicode NFC-normalized,
- duplicate object keys (ambiguous after canonicalization),
- nesting deeper than maxDepth (DoS guard).
It returns nil for a conformant document.
func VerifyCanonical ¶
VerifyCanonical reports whether sig is a valid ed25519 signature by pub over the already-canonical bytes, using COFACTORLESS strict verification that matches the Rust trust-root verifier (ed25519-dalek verify_strict). It BYPASSES the Contract-B schema gate (registry/replay use only) — prefer VerifyEntry for raw JSON.
It distinguishes two kinds of failure:
- STRUCTURAL rejects return (false, non-nil error): a wrong-length public key or signature; a small-order / undecodable public key A (a universal-forgery vector); a small-order / undecodable signature R (which ZIP-215 would accept but verify_strict rejects); a non-canonical R point encoding (y >= p, which dalek byte-compares and rejects); or a non-canonical S scalar (S >= L). These mean the input/key is malformed or hostile, not that an honest signature simply did not match.
- An honest signature non-match returns (false, nil).
A valid match returns (true, nil). It never panics.
func VerifyEntry ¶
VerifyEntry is a VALIDATED entry point: it runs the Contract-B schema gate, canonicalizes the raw JSON entry, then verifies sig against the canonical bytes using cofactorless strict verification (matching dalek verify_strict). It returns an error when the entry violates the schema, cannot be canonicalized, or is structurally rejected by VerifyCanonical (malformed key/sig, small-order pubkey); a well-formed, conformant entry whose signature simply does not match returns (false, nil).
Types ¶
type CanonicalBytes ¶
type CanonicalBytes struct {
// contains filtered or unexported fields
}
CanonicalBytes is the RFC 8785 (JCS) canonical encoding of a JSON document — the exact byte string that is signed and verified.
It is a STRUCT wrapping an unexported []byte rather than a `type CanonicalBytes []byte` alias on purpose: a named slice type still accepts a raw []byte by implicit assignment (Go assignability allows it because []byte is an unnamed type), which would leave the "signed the wrong bytes" bypass open. Wrapping the bytes in a struct closes that hole — SignCanonical/ VerifyCanonical REQUIRE a CanonicalBytes, and a raw []byte does NOT compile in their place.
The blessed constructor is Canonicalize. The only other way to mint one is CanonicalBytesFromTrusted, a deliberately-named (greppable) escape hatch for the registry/replay path that already holds known-canonical bytes (e.g. a frozen vector). Both make construction explicit, never implicit.
func CanonicalBytesFromTrusted ¶
func CanonicalBytesFromTrusted(b []byte) CanonicalBytes
CanonicalBytesFromTrusted wraps bytes the caller asserts are ALREADY JCS-canonical, WITHOUT re-canonicalizing or validating them. It is the registry/replay escape hatch (e.g. verifying a frozen vector from known canonical bytes). The name is intentionally explicit so misuse is greppable — prefer Canonicalize for any path that holds raw JSON.
func Canonicalize ¶
func Canonicalize(jsonBytes []byte) (CanonicalBytes, error)
Canonicalize returns the RFC 8785 (JCS) canonical form of the supplied JSON. The canonical form sorts object keys lexicographically by their UTF-16 code units, emits the shortest round-trippable number form, and preserves string values as raw UTF-8 (e.g. ⭐ stays 0xE2 0xAD 0x90, never \u-escaped). The output is the exact byte string that callers must sign and verify, carried in CanonicalBytes so it cannot be confused with raw JSON.
func (CanonicalBytes) Bytes ¶
func (c CanonicalBytes) Bytes() []byte
Bytes returns the underlying canonical byte string. The returned slice aliases the internal storage; callers must not mutate it.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package anchor is the SSOT for the persisted Contract-B trust anchor: the out-of-band-confirmed {network_id, root_pubkey} pair that `identity enroll` pins on first enrollment and that a later `doctor` slice authenticates registry verification against.
|
Package anchor is the SSOT for the persisted Contract-B trust anchor: the out-of-band-confirmed {network_id, root_pubkey} pair that `identity enroll` pins on first enrollment and that a later `doctor` slice authenticates registry verification against. |
|
Package doctorclient is a LOOPBACK-PINNED, read-only HTTP client for probing the LOCAL Contract-B chat daemon's well-known/whoami endpoints from `vaultmind doctor`.
|
Package doctorclient is a LOOPBACK-PINNED, read-only HTTP client for probing the LOCAL Contract-B chat daemon's well-known/whoami endpoints from `vaultmind doctor`. |
|
Package enrollment implements the Contract-B AGENT ENROLLMENT REQUEST: an agent SELF-SIGNS a request so an admin can VERIFY proof-of-possession of the agent's identity key and then decide, out-of-band, whether to add a binding for that (slug, pubkey, key_epoch) to the trust-root registry.
|
Package enrollment implements the Contract-B AGENT ENROLLMENT REQUEST: an agent SELF-SIGNS a request so an admin can VERIFY proof-of-possession of the agent's identity key and then decide, out-of-band, whether to add a binding for that (slug, pubkey, key_epoch) to the trust-root registry. |
|
Package envelope implements Contract-B SLICE 5: signing and verifying a chat MESSAGE envelope.
|
Package envelope implements Contract-B SLICE 5: signing and verifying a chat MESSAGE envelope. |
|
Package invite implements the Contract-B agent-network INVITE token: a self-contained, UNSIGNED transport an admin emits so a future `identity enroll` can bootstrap trust in a network's root anchor.
|
Package invite implements the Contract-B agent-network INVITE token: a self-contained, UNSIGNED transport an admin emits so a future `identity enroll` can bootstrap trust in a network's root anchor. |
|
Package registry implements Contract-B SLICE 3: the trust-root REGISTRY itself — a root-signed, epoched, freshness-bounded slug→pubkey binding set plus the consumer resolve/verify surface.
|
Package registry implements Contract-B SLICE 3: the trust-root REGISTRY itself — a root-signed, epoched, freshness-bounded slug→pubkey binding set plus the consumer resolve/verify surface. |
|
Package relayclient fetches a Contract-B relay's PUBLIC trust anchor from its /.well-known/vaultmind-root endpoint.
|
Package relayclient fetches a Contract-B relay's PUBLIC trust anchor from its /.well-known/vaultmind-root endpoint. |
|
Package signer implements the Contract-B DEV-INTERIM custody signer.
|
Package signer implements the Contract-B DEV-INTERIM custody signer. |