# obsigil-test-vectors

Cross-language test vectors for the
[obsigil](https://obsigil.org/) mandate-token
format — a JWT-like token split into a public **manifest** and an
encrypted **mandate**, each an authenticated, deterministically-sealed
ciphertext (AES-SIV / AES-GCM-SIV) rendered as `b64` or `hex` text.

These vectors are consumed directly by any implementation. They are
plain JSONL files with a stable schema; no tooling is required to read
them. Because obsigil seals bytes deterministically (no nonce), a token
is an exact function of its inputs, so every vector is an exact-match
known-answer test.

## Files

- `test-vectors.jsonl` — positive vectors: per-half octets ⇄ token.
- `negative-test-vectors.jsonl` — inputs that MUST be rejected.

## Keys

Two published keys, **insecure by design** (they are published here),
for conformance testing only. Vectors reference them by role.

### Manifest key (`manifest`)

The public 64-byte manifest key pinned by the spec's Construction
section (§5.2). The manifest
half is sealed *keyless* under it — anyone can open and forge a
manifest. Hex (128 chars):

```
381284633d02ea5f35df8596b5cc4218310060468e8b465455a415174ea6e966a9f48eec4ba446ddfc8b78587895356f45a75a1ab7419454dd9f7aa8a95dbdd5
```

### Mandate key (`mandate`)

A 64-byte secret mandate key, distinct from the manifest key (the
Construction section, §5.1),
defined for these vectors as `SHA-512("obsigil test mandate key v1")`.
Hex (128 chars):

```
a341adc813cfa493412cda5900fa4ec83f20a6cdea4fe5c759f7ccdb7ffbec51e01d2ce90c592909adb2ac1cad771790353f439ac86e9b113a17f7c57f0684b0
```

Code `0` (AES-SIV) uses the full 64-byte key directly. Code `1`
(AES-GCM-SIV) derives a 32-byte key with `HKDF-Expand` (info `gcmsiv`,
no Extract; the Algorithm registry's key-material derivation, §6.1).

## Positive vectors

Each line pins the per-half **octets** (a half's canonical CBOR map,
the normative input) to one exact **token**. The mapping is
bidirectional — a sealer and an opener both reproduce it without a
serializer (the octets are sealed as given):

```json
{"encoding": "b64",
 "manifest": {"alg": "0", "octets": "a124...", "fields": {"iss": "auth.example"}},
 "mandate":  {"alg": "0", "octets": "a220...", "fields": {"exp": 4000000000, "tid": "019e..."}},
 "token": "<manifest>0.0<mandate>"}
```

- `encoding` — `b64` (separator `.`) or `hex` (separator `~`).
- `manifest` / `mandate` — each optional; an absent half is omitted
  (manifest-only and mandate-only tokens are valid). `alg` is the
  one-character algorithm code (`0` AES-SIV, `1` AES-GCM-SIV). `octets`
  is the hex of the half's plaintext: a **canonical CBOR map** (RFC 8949
  §4.2) with reserved fields at negative integer keys (`tid` -1, `exp`
  -2, `aud` -3, `sub` -4, `iss` -5) and application data at non-negative
  integer / text-string keys. `fields` is a **non-normative** decode for
  the reader. `tid` is carried as its 16-byte binary form (the
Reserved fields `tid`, §8.2).
- `token` — the exact token string.

A conforming implementation checks whichever direction it performs:

- **seal:** for each present half,
  `encode(seal(octets, key[role], alg), encoding)`; assemble the halves
  with the separator and codes — the result MUST equal `token`.
- **open:** `parse(token)`, then for each present half
  `open(decode(half_text, encoding), key[role], alg)` MUST equal
  `octets`.

The manifest half uses the `manifest` key; the mandate half uses the
`mandate` key.

## Negative vectors

Each line is an input a conforming implementation MUST reject:

```json
{"op": "verify", "token": "...", "key": "mandate", "now": 4000000001, "reason": "expired exp"}
{"op": "open-manifest", "token": "...", "reason": "manifest missing iss"}
{"op": "parse", "token": "a.b.c", "reason": "more than one separator"}
```

- `op` — the operation that must fail: `verify` (the mandate path, under
  `key` and the given policy), `open-manifest` (yields no claims), or
  `parse` (structural).
- `token` — the input.
- `key` — for `verify`, the mandate key (role keyword or hex);
  defaults to `mandate`.
- `now` / `audience` / `leeway` — optional `verify` policy
  (NumericDate; verifier identifier; clock-skew leeway in
  seconds). A conformant verifier bounds `leeway` by a small
  configured maximum (the limits-and-robustness rule of the Security
  Considerations, §16.10), so a vector pairing a large
  `leeway` with a `now` far past `exp` is still rejected.
- `reason` — informative; the rule being exercised.

Rejection is **uniform** (the uniform-failure rule of the Security
Considerations, §16.6): an implementation MUST NOT signal
*why* to the bearer. Through the obsigil CLI a rejection is exit code 1.
Categories: malformed structure (separator count, an unrecognized
separator, a degenerate half on either side), unrecognized/unsupported
algorithm code (or an algorithm-code character outside the ALG
set, the Token structure section, §4), non-canonical text encoding
(padding, length 1 mod
4, non-zero trailing bits, out-of-alphabet, uppercase/odd hex), a
half below the 17-byte floor, authentication failure (a wrong key,
including a manifest sealed under the wrong key), non-canonical
CBOR in either half (a duplicate map key, keys out of canonical
order, a non-shortest integer, length, or float, a `NaN`, an
indefinite length, trailing bytes, a disallowed map-key type at
any map depth — a byte string, nested or top-level, where only
non-negative-integer and text keys are allowed — or a text string
that is not valid UTF-8),
an unrecognized negative key (obsigil's namespace — rejected
fail-closed),
a reserved field of the wrong CBOR type (a non-integer `exp` — text or
float; an `aud` that is not a non-empty array of text strings; a
non-text `iss`/`sub`; a `tid` that is not a 16-byte byte string),
missing or non-UUIDv7 `tid` (wrong version — including the common
UUIDv4 and UUIDv8 — a non-RFC-4122 variant, or a non-16-byte length),
missing `exp`, expired `exp` (including a `now` past `exp` that an
over-large but bounded `leeway` cannot extend), `aud` mismatch or empty
`aud`, an empty mandate, a manifest missing its required `iss`, and a
manifest carrying a mandate-only reserved key such as `tid` (which
yields no claims).

## Generation

The vectors are generated from the obsigil reference CLI by
[`tools/generate.py`](tools/generate.py), which seals the chosen octets
and assembles the tokens (and self-checks every line against the CLI).
Regenerate with the `obsigil` binary on `PATH`:

```sh
OBSIGIL_BIN=/path/to/obsigil python3 tools/generate.py
```

The vectors are the canonical reference, not any single implementation
(the Conformance and test vectors section, §13).

## License

Licensed under either of

- Apache License, Version 2.0
  ([LICENSE-APACHE](LICENSE-APACHE) or
  <https://www.apache.org/licenses/LICENSE-2.0>)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or
  <https://opensource.org/licenses/MIT>)

at your option.

### Contribution

Unless you explicitly state otherwise, any contribution
intentionally submitted for inclusion in the work by you, as
defined in the Apache-2.0 license, shall be dual licensed as
above, without any additional terms or conditions.
