# valiss Wire Specification, Version 1

Status: normative. This document specifies the valiss authentication scheme at
the byte and algorithm level, independent of any implementation language. The
Go reference implementation under `valiss-go/` is the source of truth; where
this document and the code disagree, the code is canonical and this document is
in error. File:line citations to `valiss-go/` are given for traceability and
are not themselves normative.

The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT,
RECOMMENDED, MAY, and OPTIONAL are to be interpreted as described in RFC 2119
and RFC 8174.

Throughout, "base64url" means RFC 4648 base64 with the URL-safe alphabet and no
padding (Go `base64.RawURLEncoding`). "base64std" means RFC 4648 base64 with the
standard alphabet and padding (Go `base64.StdEncoding`). "base32" means RFC 4648
base32 with the standard uppercase alphabet (`A`–`Z`, `2`–`7`) and no padding
(Go `base32.StdEncoding.WithPadding(base32.NoPadding)`). "SHA-256" is FIPS 180-4.
"Ed25519" is RFC 8032. Byte strings shown in `code font` are exact; `\n` denotes
a single line-feed (0x0A) byte and no other whitespace is implied.

---

## 1. Overview

valiss provides offline-verifiable delegated tenant authentication. A server
can authenticate a request, and attribute it to a tenant and an end user,
knowing only one pinned public key: the operator's. No network round-trip to an
issuer is required to verify a credential.

### 1.1 Key hierarchy

Identities are Ed25519 key pairs encoded in the NATS nkey text format
(section 3.6). There are three delegation levels plus an optional per-message
level:

1. **Operator.** Holds an operator nkey. Its public key is the trust anchor
   that servers pin. The operator MAY publish a self-signed *operator token*
   stating trust-domain policy (epoch, validity window, extensions).
2. **Account (tenant).** Holds an account nkey. The operator signs each tenant
   an *account token* whose subject is the tenant's account public key. Issued
   account tokens are recorded in a server-side allowlist (section 6.4) keyed by
   token id, so a token can be revoked before its expiry.
3. **User.** Holds a user nkey. A tenant MAY delegate to end users by signing
   *user tokens* with its account key.
4. **Message (optional).** A user key MAY additionally mint per-message tokens:
   short-lived, self-signed proofs of origin binding a token to a destination
   and a payload checksum. Message tokens are proofs, never credentials:
   possession grants nothing, and a request verifier MUST NOT accept one
   (`valiss-go/message.go:26-27`, `valiss-go/verifier.go` accepts only
   account/user tokens).

The public key at each level is the on-wire identity. A subject proves
possession of its private key either per request (a request signature,
section 5) or, for bearer user tokens, by presenting the token alone.

Key roles are strict (`valiss-go/token.go` issue/verify functions,
`valiss-go/docs/VERIFYING.md` "Role requirements"):

- An operator key signs account tokens; an operator token is self-signed
  (`iss == sub`).
- An account key signs user tokens.
- A user key signs message tokens; a message token is self-signed
  (`iss == sub`).

A verifier MUST reject a token whose issuer or subject key is of the wrong role
for its level (section 6).

### 1.2 The three serialized artifacts

The scheme defines three independently versioned wire artifacts:

- **Tokens** — nkey-signed JWTs (sections 2, 3).
- **Credentials file** — a client's token(s) plus its seed (section 4).
- **Request signature** — the per-request proof of possession (section 5).

Each carries its own version discriminator (section 8). This document specifies
version 1 of all three.

---

## 2. Wire version and envelope

### 2.1 JWS-compact envelope

Every token, of any version, is a JWS Compact Serialization string of exactly
three parts separated by ASCII period (`.`):

```
base64url(header) "." base64url(payload) "." base64url(signature)
```

All three parts MUST use base64url (no padding) (`valiss-go/token.go:169-175`,
`227-252`). A verifier MUST reject a token that does not split into exactly
three `.`-separated parts (`valiss-go/token.go:185-187`).

### 2.2 Header

For version 1 the header, before base64url encoding, MUST be the byte-exact
JSON:

```json
{
  "typ": "JWT",
  "alg": "ed25519-nkey",
  "ver": 1
}
```

This exact byte string is the frozen version-1 header
(`valiss-go/token.go:33`). Producers MUST emit it verbatim.

A verifier reads the header before parsing the payload
(`valiss-go/token.go:183-205`, `peekVersion`). It:

1. base64url-decodes part 1 and parses it as JSON.
2. MUST reject unless `typ == "JWT"` and `alg == "ed25519-nkey"`
   (`valiss-go/token.go:201-203`).
3. Reads the integer `ver` and dispatches to the matching per-version parser.
   Version 1 is parsed per section 3. An unrecognized `ver` MUST be rejected
   without parsing the payload (`valiss-go/token.go:216-221`).

A verifier MUST NOT skip signature verification on the basis of the header:
the header is unauthenticated until the signature over it is checked, so version
dispatch selects the parser but the signature MUST always be verified by that
parser (`valiss-go/token.go` per-version decoders always verify; section 8).
Because `alg` identifies the algorithm and `ver` identifies the format version,
a change to the payload layout is expressed by bumping `ver`, never by
overloading `alg`.

### 2.3 Signing input and signature

The signing input is the ASCII bytes:

```
base64url(header) "." base64url(payload)
```

that is, parts 1 and 2 of the envelope joined by a period, exactly as they
appear on the wire (`valiss-go/token.go:169-170`). The signature is the raw
64-byte Ed25519 signature of the signing input under the issuer's private key,
base64url-encoded as part 3 (`valiss-go/token.go:171-175`).

To verify, a verifier reconstructs the signing input as `part1 "." part2`
(the received bytes, not a re-serialization), decodes the issuer public key from
the payload's `iss` field (section 3.6), and verifies the Ed25519 signature
(`valiss-go/token.go:243-253`). Verification against the issuer embedded in the
token establishes authenticity only, not trust; trust is a chain-level check
(section 6).

---

## 3. Token format

A token payload is a JSON object using RFC 7519 registered claim names for its
standard fields, plus a nested `valiss` object carrying the level-specific
claim body.

### 3.1 Field set and order

The version-1 payload object MUST serialize its fields in exactly this order
(`valiss-go/token.go:115-125`, the `wireV1` struct):

| # | JSON name | JSON type | omit-when-empty     | meaning                                |
|---|-----------|-----------|---------------------|----------------------------------------|
| 1 | `jti`     | string    | yes                 | content-derived token id (section 3.5) |
| 2 | `iat`     | number    | yes (omit when 0)   | issued-at, Unix seconds                |
| 3 | `iss`     | string    | yes                 | issuer public key, nkey-encoded        |
| 4 | `name`    | string    | yes                 | human-readable subject label           |
| 5 | `sub`     | string    | yes                 | subject public key, nkey-encoded       |
| 6 | `aud`     | string    | yes                 | audience/destination (message tokens)  |
| 7 | `exp`     | number    | yes (omit when 0)   | expiry, Unix seconds                   |
| 8 | `nbf`     | number    | yes (omit when 0)   | not-before, Unix seconds               |
| 9 | `valiss`  | object    | no (always present) | level-specific body (section 3.4)      |

Field order is fixed and load-bearing: the `jti` derivation (section 3.5)
depends on byte-identical serialization. Every standard field except `valiss`
is omit-when-empty, so a level that does not set a field emits no bytes for it,
leaving other levels' `jti` derivations unaffected.

`iat`, `exp`, and `nbf` are signed 64-bit integers of seconds since the Unix
epoch. `exp == 0` (absent) means the token never expires; `nbf == 0` (absent)
means it is valid immediately (`valiss-go/claims.go:437-449`).

### 3.2 JSON serialization rules

To reproduce a byte-identical payload (required for `jti`, section 3.5), a
producer MUST serialize with:

- No insignificant whitespace between tokens.
- Fields in the order of section 3.1; nested object fields in the order given
  by their body definition (section 3.4).
- Go `encoding/json` string escaping. In particular the characters `<`, `>`,
  and `&` are escaped as `<`, `>`, `&` respectively
  (`valiss-go/docs/VERIFYING.md` jti derivation). Implementations in other
  languages MUST replicate this HTML-escaping to reproduce `jti`.

A verifier that only checks signatures MAY treat `jti` as an opaque string and
need not reproduce this serialization; a verifier that re-derives `jti` (for
example to dedup keyring entries) MUST.

### 3.3 The `valiss` body and `type` discriminator

The `valiss` object is discriminated by its string field `type`, whose value is
one of (`valiss-go/token.go:35-38`):

| `type` value | level    | signing rule                                 |
|--------------|----------|----------------------------------------------|
| `"operator"` | operator | self-signed, `iss == sub`, operator-role key |
| `"account"`  | account  | signed by an operator-role key               |
| `"user"`     | user     | signed by an account-role key                |
| `"message"`  | message  | self-signed, `iss == sub`, user-role key     |

A verifier MUST reject a token whose `valiss.type` does not match the level it
expects (`valiss-go/token.go:356-357`, `381`, `406`; `valiss-go/message.go:294-295`).

### 3.4 Per-level bodies

Field names and types are exact. All fields except `type` are omit-when-empty.
The `ext` field is an object mapping extension names to arbitrary JSON values,
carried opaquely by the scheme (section 3.7).

**Operator body** (`valiss-go/token.go:43-51`), fields in order:

| name    | type            | notes                                   |
|---------|-----------------|-----------------------------------------|
| `type`  | string          | `"operator"`                            |
| `epoch` | number (uint64) | trust-domain current epoch; omit when 0 |
| `ext`   | object          | extension claims; omit when empty       |

**Account body** (`valiss-go/token.go:60-67`), fields in order:

| name    | type            | notes                                      |
|---------|-----------------|--------------------------------------------|
| `type`  | string          | `"account"`                                |
| `epoch` | number (uint64) | epoch the token was issued in; omit when 0 |
| `ext`   | object          | extension claims; omit when empty          |

**User body** (`valiss-go/token.go:70-80`), fields in order:

| name     | type            | notes                                                                                   |
|----------|-----------------|-----------------------------------------------------------------------------------------|
| `type`   | string          | `"user"`                                                                                |
| `epoch`  | number (uint64) | epoch; omit when 0                                                                      |
| `bearer` | boolean         | `true` marks a token the server accepts without per-request signatures; omit when false |
| `ext`    | object          | extension claims; omit when empty                                                       |

**Message body** (`valiss-go/token.go:94-106`), fields in order:

| name       | type            | notes                                                         |
|------------|-----------------|---------------------------------------------------------------|
| `type`     | string          | `"message"`                                                   |
| `epoch`    | number (uint64) | epoch; omit when 0                                            |
| `checksum` | string          | lowercase-hex SHA-256 of the message payload; omit when empty |
| `chain`    | object          | embedded provenance chain (below); omit when absent           |
| `ext`      | object          | extension claims; omit when empty                             |

The message `chain` object (`valiss-go/token.go:85-90`), fields in order:

| name      | type   | notes                                                                   |
|-----------|--------|-------------------------------------------------------------------------|
| `account` | string | the operator-signed account token, verbatim; omit when empty            |
| `user`    | string | the account-signed user token of the emitter, verbatim; omit when empty |

`epoch` is `uint64`. `epoch == 0` (absent) is the default epoch. When a verifier
enforces an epoch (section 6.5), every level in a chain MUST agree on it.

### 3.5 `jti` derivation

`jti` is content-derived, not random (`valiss-go/token.go:158-164`):

1. Build the payload object with `jti` set to the empty string (so, being
   omit-when-empty, it emits no bytes), all other fields set to their final
   values including `iss` and `iat`.
2. Serialize it to JSON per section 3.2. Call these bytes `U`.
3. Compute `digest = SHA-256(U)` (32 bytes).
4. `jti = base32(digest)` (RFC 4648 uppercase alphabet, no padding; 52
   characters).
5. Re-serialize the payload with `jti` now set to that value to obtain the final
   payload bytes that are signed (`valiss-go/token.go:165-168`).

Because `jti` is content-derived, two tokens with identical claims (including
the same `iat` second) share a `jti`; keyring de-duplication relies on this
(`valiss-go/keyring.go:57-59`).

### 3.6 nkey encoding of `iss` and `sub`

`iss` and `sub` carry Ed25519 public keys in the NATS nkey text format
(`valiss-go/docs/VERIFYING.md` "Decoding an nkey public key"):

```
base32( prefix_byte(1) || raw_ed25519_public_key(32) || crc16_le(2) )
```

- base32: RFC 4648 uppercase alphabet, no padding.
- The single prefix byte encodes the role. Public-key prefixes:
  operator = `112` (renders with leading `O`), account = `0` (`A`),
  user = `160` (`U`). Seed prefixes render as `SO`, `SA`, `SU`.
- The CRC-16 is CCITT/XMODEM (polynomial `0x1021`, init `0x0000`) computed over
  `prefix_byte || raw_public_key` (the first 33 bytes) and appended
  little-endian.

To decode: base32-decode; require exactly 35 bytes; verify the trailing 2-byte
CRC-16 over the first 33 bytes; check the prefix byte matches the role required
by context; take bytes 1..33 as the raw 32-byte Ed25519 public key. A verifier
MUST perform the role check (`nkeys.IsValidPublicOperatorKey` /
`...AccountKey` / `...UserKey` at `valiss-go/token.go:362`, `387`, `412`;
`valiss-go/message.go:300`).

### 3.7 Extensions (`ext`)

`ext` maps a string extension name to an arbitrary JSON value
(`valiss-go/token.go:57`). The scheme signs and transports extension values
untouched and assigns them no meaning; meaning is defined by whoever registered
the name (a transport layer, or a consumer). A producer MUST NOT emit two
entries with the same name (`valiss-go/claims.go:219-221`). A verifier MAY
require named extensions to decode into an expected shape (section 6.9); absent
that, it MUST carry them through opaquely.

### 3.8 Level constraints on standard fields

- Operator, account, and user tokens carry `name`; message tokens MUST NOT
  (`valiss-go/message.go:97-99`).
- Only message tokens carry `aud` (`valiss-go/claims.go:167-173`, `238-241`).
- `bearer` is valid only on user tokens
  (`valiss-go/claims.go:262-263`, `295-296`; `valiss-go/message.go:94-96`).
- Message tokens MUST carry an `exp` (`valiss-go/message.go:100-102`); they are
  short-lived proofs. The reference transports mint them with a 30-second TTL
  (`DefaultMessageTTL`, `valiss-go/message.go:16`).
- When `name` is absent, a consumer displays the subject public key in its
  place (`valiss-go/claims.go:451-457`); this is a presentation fallback, not a
  wire value.

---

## 4. Credentials file format

A credentials ("creds") file is a marker-delimited UTF-8 text file holding a
subject's token(s) and the seed that signs its requests
(`valiss-go/creds/creds.go`).

### 4.1 Version header

The file SHOULD begin with a version line (`valiss-go/creds/creds.go:26-29`,
`57-59`):

```
VALISS-CREDS-VERSION: 1
```

The marker string is `VALISS-CREDS-VERSION:` and the version for this spec is
`1`. A parser MUST read this line, if present, before parsing the payload, and
MUST reject a version it does not implement (`valiss-go/creds/creds.go:124-140`).
An absent header MUST be read as the current version (the pre-versioned format
is otherwise identical). This header versions the file container only; the
tokens inside carry their own token version (section 2).

### 4.2 Section markers

Three optional sections are delimited by these exact marker lines
(`valiss-go/creds/creds.go:33-39`). Note the asymmetry: BEGIN markers use five
leading and trailing dashes; END markers use six.

| section       | BEGIN marker                           | END marker                             |
|---------------|----------------------------------------|----------------------------------------|
| account token | `-----BEGIN VALISS ACCOUNT TOKEN-----` | `------END VALISS ACCOUNT TOKEN------` |
| user token    | `-----BEGIN VALISS USER TOKEN-----`    | `------END VALISS USER TOKEN------`    |
| seed          | `-----BEGIN VALISS SEED-----`          | `------END VALISS SEED------`          |

### 4.3 Parse rules

- Each present section MUST contain exactly one non-empty payload line strictly
  between its BEGIN and END markers (`valiss-go/creds/creds.go:148-175`,
  `between`). Blank lines inside a section are ignored. A section that is empty,
  unclosed, or holds more than one payload line MUST be rejected.
- Lines are matched after trimming surrounding whitespace.
- Content outside any section (including the version line and any human-readable
  notes) is ignored during payload extraction.
- At least one token section (account or user) MUST be present; a file with no
  token MUST be rejected (`valiss-go/creds/creds.go:98-100`).
- The seed section is OPTIONAL. A creds file with tokens but no seed is a
  *bearer* creds file: its holder cannot sign requests, so the server accepts it
  only when the effective token is a bearer user token
  (`valiss-go/creds/creds.go:8-11`).

### 4.4 Creds shapes

- **Account-level creds**: an account token plus the account seed.
- **User-level creds**: a user token plus the user seed; the account token is
  resolved server-side (section 6.2).
- **Bundle**: user-level creds that additionally embed the upstream account
  token, for servers that do not resolve it.
- **Bearer creds**: token(s) only, no seed.

The seed line, when present, holds the nkey seed text (an `S…`-prefixed string)
that signs requests as the file's subject: the account seed in account-level
creds, the user seed in user-level creds.

### 4.5 Rendering

When rendering, a producer emits the version line, then any present sections
separated by a single blank line, each as BEGIN / single payload line / END,
in the order account token, user token, seed
(`valiss-go/creds/creds.go:57-75`). A producer MAY append a human-readable
warning after the seed; parsers MUST ignore it.

---

## 5. Request signature format

A request signature is a per-request proof that the sender holds the subject's
private key. It is used by the credential transports; bearer requests carry no
signature (section 6.7).

### 5.1 Canonical signed bytes

The signed byte string for version 1 is (`valiss-go/sign.go:23`, `41-44`):

```
"valiss-req-v1\n" + timestamp + "\n" + lowercasehex(SHA-256(request_context))
```

where:

- `"valiss-req-v1\n"` is the literal version tag including its trailing
  line-feed. It is part of the signed bytes, so a signature made under any other
  version cannot match a v1 reconstruction; v1 therefore fails closed against
  version confusion without any transport-carried version signal
  (`valiss-go/sign.go:20-23`).
- `timestamp` is the request time formatted as RFC 3339 with nanosecond
  precision, in UTC (Go `time.RFC3339Nano` after `.UTC()`,
  `valiss-go/sign.go:43`, `56`).
- `request_context` is the transport's canonical byte description of the request
  (section 5.3). Its SHA-256 digest is rendered as lowercase hexadecimal.

The signature is the raw 64-byte Ed25519 signature of these bytes under the
subject's private key.

### 5.2 Transmission

The transport transmits two values (`valiss-go/sign.go:51-57`):

- **timestamp**: the same RFC 3339 nanosecond UTC string used in the signed
  bytes.
- **signature**: the raw signature encoded with base64std (standard alphabet,
  with padding) — note this differs from the base64url used inside tokens.

The reference transports carry these as `valiss-timestamp` and
`valiss-signature` headers / metadata, alongside `valiss-account-token`,
`valiss-user-token`, and optionally `valiss-nonce`
(`valiss-go/verifier.go:11-31`).

### 5.3 request_context

`request_context` is transport-defined and MUST be reconstructed identically by
client and server; a mismatch fails the signature. The reference transports use
(`valiss-go/contrib/httpauth/transport.go:93-99`,
`valiss-go/contrib/grpcauth/credentials.go:98`):

- **HTTP**: `"http\n" + method + "\n" + host + "\n" + path + "\n" + nonce`.
  The query string is excluded; `host` is the request Host (falling back to the
  URL host); `path` is matched exactly.
- **gRPC**: `"grpc\n" + full_method + "\n" + nonce`.

`nonce` is the empty string when replay suppression is not in use. A transport
MAY define its own `request_context`; passing empty context binds nothing beyond
the version tag and timestamp (`valiss-go/sign.go:50`).

### 5.4 Skew window check

On verification (`valiss-go/sign.go:63-83`):

1. Parse `timestamp` as RFC 3339 nanosecond. A malformed timestamp MUST be
   rejected.
2. Compute `d = now - timestamp`. If `d > skew` or `d < -skew` (a symmetric
   window), the request MUST be rejected. The default `skew` is 2 minutes
   (`DefaultSkew`, `valiss-go/sign.go:15`).
3. base64std-decode the signature; a decode failure MUST be rejected.
4. Decode the subject public key (nkey) and verify the Ed25519 signature over
   the bytes of section 5.1 reconstructed from the received timestamp and the
   locally derived `request_context`.

---

## 6. Verification algorithm

This section specifies the full per-request verification a server MUST perform,
and the message-token verification. Any failing check MUST cause the request to
be rejected as unauthenticated; a verifier MUST NOT return a partial identity.

### 6.1 Inputs

A verifier is configured with either:

- a single pinned operator public key, optionally plus the operator's
  self-signed token to enforce policy (`valiss-go/verifier.go:219-236`); or
- a *keyring* of trusted operator tokens (section 6.6,
  `valiss-go/verifier.go:250-265`).

Plus an allowlist (section 6.4), an optional account-token resolver
(section 6.2), an optional replay cache (section 6.8), optional extension-type
checks (section 6.9), and optional custom validators.

A request carries: an account token (optional if a resolver is configured), an
optional user token, an optional timestamp+signature (+nonce), and the
`request_context` bytes.

### 6.2 Resolving the account token

If the request carries no account token
(`valiss-go/verifier.go:279-295`):

- If it also carries no user token, reject (`missing`).
- Otherwise, if no resolver is configured, reject (`no_resolver`).
- Otherwise derive the account public key as the user token's issuer
  (`IssuerOf`, which verifies the user token against its own embedded issuer)
  and ask the resolver for the matching account token. The resolved token then
  goes through the full verification below (signature, epoch, expiry,
  allowlist).

### 6.3 Establishing the trust anchor and verifying the account token

**Single-anchor verifier** (`valiss-go/verifier.go:315-320`): verify the account
token with `iss == pinned operator key`, `type == "account"`, and `sub` a valid
account-role nkey (`valiss-go/token.go:376-396`). A wrong issuer, wrong type, or
bad signature MUST be rejected.

**Keyring verifier** (`valiss-go/verifier.go:302-314`): read the account token's
own issuer, verify the token against that issuer, then look up the keyring entry
for `(issuer key, account.epoch)`. If no such entry exists, reject
(`unknown_operator`). The looked-up entry becomes the operator policy for the
rest of verification. Entry selection is by name, not trial: the credential
names its operator and epoch.

### 6.4 Allowlist

The account token's `jti` (its `ID`) MUST be present in the allowlist, or the
request MUST be rejected (`valiss-go/verifier.go:339-341`;
`valiss-go/allowlist.go`). The allowlist is the revocation mechanism: removing a
`jti` revokes that account token before its `exp`. A development-only
`AllowAll` accepts everything (`valiss-go/allowlist.go:74-78`); signature and
expiry still gate access.

### 6.5 Epoch enforcement

When an operator policy is in force (a configured operator token on a
single-anchor verifier, or any keyring entry), the verifier MUST
(`valiss-go/verifier.go:322-332`, `348-350`):

- reject if the operator token is expired or not-yet-valid (with skew,
  section 6.10);
- require `account.epoch == operator.epoch`;
- require `user.epoch == operator.epoch` when a user token is present.

Bumping the operator epoch and re-minting therefore rotates the whole trust
domain at once. When no operator policy is configured on a single-anchor
verifier, epochs are not enforced. (A misconfigured operator token, one not
self-signed by the pinned key, MUST poison the verifier so every request fails
rather than silently skipping policy: `valiss-go/verifier.go:276-278`,
`219-223`.)

### 6.6 User token

If the request carries a user token (`valiss-go/verifier.go:343-358`), verify it
with `iss == account.sub` (the delegating account key), `type == "user"`, and
`sub` a valid user-role nkey (`valiss-go/token.go:401-422`). Enforce its epoch
(section 6.5) and its validity window (section 6.10). The effective subject for
the request signature is the user token's `sub`; without a user token it is the
account token's `sub` (`valiss-go/verifier.go:363-366`).

### 6.7 Request signature / bearer

Possession MUST be proven before any consumer-supplied hook runs
(`valiss-go/verifier.go:360-383`):

- If both timestamp and signature are empty, the request is a *bearer* request.
  It is accepted only when a user token is present and its `bearer` flag is
  true; otherwise reject (`not_bearer`). Account-level requests MUST always
  sign.
- Otherwise verify the request signature (section 5) against the effective
  subject key within the skew window. A failure MUST be rejected.

### 6.8 Replay / nonce

When a replay cache is configured (`valiss-go/verifier.go:375-382`):

- A signed request MUST carry a nonce; a missing nonce MUST be rejected.
- If the cache reports the nonce as already seen, reject (`replay`).
- Otherwise record the nonce with an expiry of `now + 2*skew` (the longest a
  replay could still land inside a valid timestamp window). The nonce is folded
  into `request_context` (section 5.3) so it is covered by the signature. Bearer
  requests, carrying no signature, are unaffected.

A nonce is a per-request unique value; the reference client uses 128 random bits
rendered as hex (`valiss-go/sign.go:26-33`).

### 6.9 Extension checks and validators

After possession is proven, the verifier runs, in order
(`valiss-go/verifier.go:384-398`):

1. registered extension-type checks against both the account and user token
   `ext` maps: a present extension MUST decode into its registered shape or the
   request is rejected;
2. custom validators in registration order; the first error rejects the request.

### 6.10 Validity windows and skew

For a claims set with expiry `exp` and not-before `nbf`
(`valiss-go/claims.go:459-468`), given the verification instant `now` and skew
`skew`:

- **expired** iff `exp` is present and `now > exp + skew`;
- **not yet valid** iff `nbf` is present and `now + skew < nbf`.

Absent `exp`/`nbf` impose no constraint. The default skew is 2 minutes
(`DefaultSkew`). These checks apply to the operator, account, user, and message
tokens as each appears in a chain.

### 6.11 Result

On success the verifier returns the verified identity: the account claims,
the user claims (if any), and the operator claims (the keyring entry, or the
configured operator token, else none) (`valiss-go/verifier.go:342`, `356`,
`399`; `valiss-go/verifier.go:59-71`). Consumers segment tenant data by the
account identity and distinguish trust domains by the operator name.

### 6.12 Message-token verification

A message token is verified against a pinned operator key (or a keyring) as a
full chain (`valiss-go/message.go:232-386`; `valiss-go/docs/VERIFYING.md`
"Verifying a message token"):

1. Decode and verify the message token: `type == "message"`, `iss == sub`,
   `sub` a valid user-role nkey (`valiss-go/message.go:294-302`).
2. Obtain the chain: the token's embedded `valiss.chain`, or a chain supplied
   out of band. If neither exists, fail with the distinct `no_chain` condition
   (`ErrNoChain`, `valiss-go/message.go:22`, `304-306`) so a receiver can
   request retransmission with the chain. If both exist they MUST match exactly,
   field for field (`valiss-go/message.go:308-311`).
3. Verify the chain account token: `type == "account"`, signed by a trusted
   operator key (the pinned key, or, for a keyring, the entry selected by the
   token's issuer and epoch), `sub` an account-role nkey
   (`valiss-go/message.go:233-246`, `261-278`).
4. Verify the chain user token: `iss == account.sub`, `type == "user"`, `sub` a
   user-role nkey (`valiss-go/message.go:320-323`).
5. Require the chain user token's `sub` to equal the message token's `iss` (the
   chain must delegate to exactly the signing key)
   (`valiss-go/message.go:324-326`).
6. Require the message, account, and user tokens to agree on `epoch`. If an
   operator policy is in force, additionally require the message epoch to equal
   the operator epoch and the operator token to be within its validity window
   (`valiss-go/message.go:327-343`).
7. Check every validity window at the verification instant (which for a stored
   message SHOULD be the receipt instant, not the current time)
   (`valiss-go/message.go:354-371`).
8. If an expected audience is configured, require `aud` to equal it; a token
   bound to a different audience, or to none, MUST be rejected
   (`valiss-go/message.go:372-374`).
9. If a payload is supplied, require `valiss.checksum` to equal the lowercase-hex
   SHA-256 of the payload as received; a token without a checksum MUST be
   rejected when a payload or an explicit checksum requirement is present
   (`valiss-go/message.go:375-384`). Checksum is computed as
   `lowercasehex(SHA-256(payload))` (`valiss-go/message.go:57-60`).

A verified message token proves origin only and MUST NOT be treated as a
credential.

---

## 7. Reason codes / error taxonomy

A conformant verifier distinguishes the following failure conditions so that
negative conformance tests can assert the same reason across implementations.
The short code is stable; the human message is illustrative. Codes are grouped
by the stage that raises them.

### 7.1 Envelope / decode

| code                  | condition                                                                                                     | reference                                          |
|-----------------------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------|
| `malformed`           | not three `.`-separated parts; header/payload/signature not valid base64url; header or payload not valid JSON | `valiss-go/token.go:185-199`, `228-231`, `247-250` |
| `unsupported_type`    | header `typ != "JWT"` or `alg != "ed25519-nkey"`                                                              | `valiss-go/token.go:201-203`                       |
| `unsupported_version` | header `ver` is not a supported wire version                                                                  | `valiss-go/token.go:219-221`                       |
| `bad_issuer_key`      | `iss` is not a decodable nkey public key                                                                      | `valiss-go/token.go:243-245`                       |
| `bad_signature`       | Ed25519 verification over the signing input fails                                                             | `valiss-go/token.go:251-252`                       |

### 7.2 Token semantics

| code                 | condition                                                                                                                                                                     | reference                                                          |
|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
| `wrong_type`         | `valiss.type` is not the expected level                                                                                                                                       | `valiss-go/token.go:356`, `381`, `406`; `message.go:294`           |
| `wrong_issuer`       | `iss` is not the expected issuer key for the level (operator not self-signed; account not operator-signed; user not account-signed; message not signed by the chain user key) | `valiss-go/token.go:359`, `384`, `409`; `message.go:297`, `324`    |
| `wrong_subject_role` | `sub` is not a valid nkey of the level's role                                                                                                                                 | `valiss-go/token.go:362`, `387`, `412`; `message.go:300`           |
| `expired`            | now past `exp + skew` (operator, account, user, or message)                                                                                                                   | `valiss-go/verifier.go:323`, `333`, `351`; `message.go` windows    |
| `not_yet_valid`      | now before `nbf - skew`                                                                                                                                                       | `valiss-go/verifier.go:326`, `336`, `354`                          |
| `epoch_mismatch`     | a chain level's epoch disagrees with the operator or another level                                                                                                            | `valiss-go/verifier.go:329`, `348`; `message.go:334`, `338`, `341` |

### 7.3 Request / credential

| code                     | condition                                                               | reference                                             |
|--------------------------|-------------------------------------------------------------------------|-------------------------------------------------------|
| `missing`                | neither account nor user token present                                  | `valiss-go/verifier.go:280-281`                       |
| `no_resolver`            | user-only request but no account-token resolver configured              | `valiss-go/verifier.go:283-285`                       |
| `unknown_operator`       | keyring has no entry for the credential's `(issuer, epoch)`             | `valiss-go/verifier.go:310-313`; `message.go:273-276` |
| `not_allowlisted`        | account token `jti` not in the allowlist                                | `valiss-go/verifier.go:339-341`                       |
| `not_bearer`             | no request signature and the effective token is not a bearer user token | `valiss-go/verifier.go:367-369`                       |
| `skew`                   | request timestamp outside the symmetric skew window, or unparsable      | `valiss-go/sign.go:64-70`                             |
| `bad_signature_encoding` | request signature not valid base64std                                   | `valiss-go/sign.go:71-73`                             |
| `bad_request_signature`  | request Ed25519 signature verification fails                            | `valiss-go/sign.go:79-81`                             |
| `nonce_required`         | replay cache configured but signed request carries no nonce             | `valiss-go/verifier.go:376-377`                       |
| `replay`                 | nonce already seen within the retention window                          | `valiss-go/verifier.go:379-380`                       |
| `operator_misconfigured` | configured operator token is not self-signed by the pinned key          | `valiss-go/verifier.go:276-277`, `219-223`            |
| `extension_invalid`      | a registered extension fails to decode                                  | `valiss-go/verifier.go:385-392`                       |
| `validator_rejected`     | a custom validator returned an error                                    | `valiss-go/verifier.go:394-398`                       |

### 7.4 Message-specific

| code                  | condition                                                                      | reference                                 |
|-----------------------|--------------------------------------------------------------------------------|-------------------------------------------|
| `no_chain`            | message token embeds no chain and none was supplied                            | `valiss-go/message.go:22`, `305-306`      |
| `chain_mismatch`      | embedded and supplied chains differ                                            | `valiss-go/message.go:309-311`            |
| `chain_user_mismatch` | chain user `sub` does not equal message `iss`                                  | `valiss-go/message.go:324-326`            |
| `wrong_audience`      | `aud` does not equal the expected audience (or is absent when one is expected) | `valiss-go/message.go:372-374`            |
| `checksum_missing`    | payload/checksum required but token carries no `checksum`                      | `valiss-go/message.go:376-377`, `382-383` |
| `checksum_mismatch`   | `checksum` does not match the payload hash                                     | `valiss-go/message.go:379-381`            |

An implementation MAY use finer subdivisions internally but MUST be able to map
each of its failures to one of the codes above for cross-implementation
conformance testing.

---

## 8. Versioning and forward-compatibility

The rules of ADR 0009 are normative:

1. **The JWS-compact envelope is fixed across all versions.** Every version is
   `base64url(header) "." base64url(payload) "." base64url(signature)` with a
   JSON header. `ver` lives in the header and is only readable while that shape
   holds; committing to the shape is deliberate.
2. **The version selects the parser; the signature is always verified.** A
   verifier reads `ver` from the (unauthenticated) header and dispatches to the
   matching per-version parser, then that parser verifies the signature. A
   verifier MUST NOT skip verification based on the header: a forged version
   either routes to a real parser that rejects the bad signature, or is an
   unknown version rejected outright. Because the request-signature version tag
   is part of the signed bytes (section 5.1), downgrade is blocked.
3. **Adding a version is additive.** A new version adds a new set of wire types,
   a new decoder that normalizes into the version-neutral internal view, and one
   dispatch case; nothing outside that set changes. `alg` MUST NOT be overloaded
   to express a format-version change.
4. **Each artifact versions independently.** The token `ver` header
   (section 2.2), the `VALISS-CREDS-VERSION` line (section 4.1), and the
   `valiss-req-v1` signed prefix (section 5.1) advance separately; the creds
   container version is independent of the token version it carries.

For version 1 there is no second version to coexist with, so the request
signature carries no transport-level version signal: it fails closed by way of
the signed prefix. A second version would introduce explicit dispatch (for
example a transport `valiss-version` header) as an additive step.

---

## Appendix A. Constants summary

| constant                         | value                                                     | reference                        |
|----------------------------------|-----------------------------------------------------------|----------------------------------|
| wire version                     | `1`                                                       | `valiss-go/token.go:32`          |
| token header (v1)                | `{"typ":"JWT","alg":"ed25519-nkey","ver":1}`              | `valiss-go/token.go:33`          |
| request signed prefix (v1)       | `valiss-req-v1\n`                                         | `valiss-go/sign.go:23`           |
| creds version marker             | `VALISS-CREDS-VERSION:` = `1`                             | `valiss-go/creds/creds.go:26-29` |
| default skew                     | 2 minutes                                                 | `valiss-go/sign.go:15`           |
| default message TTL (transports) | 30 seconds                                                | `valiss-go/message.go:16`        |
| replay retention                 | `2 * skew`                                                | `valiss-go/verifier.go:379`      |
| nkey public prefixes             | operator `112` (`O`), account `0` (`A`), user `160` (`U`) | `valiss-go/docs/VERIFYING.md`    |
| jti encoding                     | base32 (RFC 4648 upper, no pad) of SHA-256                | `valiss-go/token.go:164`         |
| request timestamp format         | RFC 3339 nanosecond, UTC                                  | `valiss-go/sign.go:43`, `56`     |
| token part encoding              | base64url, no padding                                     | `valiss-go/token.go:169-175`     |
| request signature encoding       | base64std, padded                                         | `valiss-go/sign.go:56`           |
