identity

package
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 1, 2026 License: MIT Imports: 13 Imported by: 0

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:

  1. 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).
  2. A small-order (or undecodable) signature R component. ZIP-215 would accept these; verify_strict (and so this code) rejects them.
  3. 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

View Source
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.

View Source
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

func ValidateSchema(jsonBytes []byte) error

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

func VerifyCanonical(pub ed25519.PublicKey, canonical CanonicalBytes, sig []byte) (bool, error)

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

func VerifyEntry(pub ed25519.PublicKey, jsonBytes []byte, sig []byte) (bool, error)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL