feat: add protocol test vectors generated from the reference implementation

Known-answer tests for every part of the protocol that produces bytes: key
derivation, sealed envelopes with their intermediate values, accept tokens,
session and registration frames, rotation certificates, frontmatter parsing,
and the inputs a conformant implementation must refuse.

vectors.py derives every value by calling smolmail.py, so the vectors cannot
drift from the code, and verifies each one from the opposite side before
emitting it. --check regenerates and diffs against the committed files.

Verified independently: a checker written against SPEC.md and libsodium alone,
without importing the reference, reproduces all 81 values.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
randogoth 2026-10-14 16:05:23 +03:00
parent 43e4be2b8a
commit 9d5908104e
4 changed files with 2216 additions and 1 deletions

View file

@ -84,6 +84,19 @@ A security review of the protocol and these reference implementations found no i
[SPEC.md](SPEC.md) defines the wire format, the operations and the trust model.
## Test vectors
[VECTORS.md](VECTORS.md) is a set of known-answer tests for every part of the protocol that produces bytes: key derivation, sealed envelopes with their intermediate values, accept tokens, the session and registration frames, rotation certificates, frontmatter parsing, and the inputs a conformant implementation must refuse. [vectors.json](vectors.json) carries the same values for a test harness to load.
[vectors.py](vectors.py) generates both from `smolmail.py`, so they cannot drift from the code, and verifies every vector from the opposite side before emitting it. `uv run vectors.py --check` regenerates and compares against the committed files:
```
uv run vectors.py # write vectors.json and VECTORS.md
uv run vectors.py --check # verify the committed files still reproduce
```
Start from the `unpadded` envelope. If its `seal_key` matches and the envelope does not, the fault is in the payload layout; if `x25519_shared` already differs, it is in the §2 key map.
## Inspiration
The smolmail project takes inspiration from [Gemini Protocol](https://geminiprotocol.net), [Misfin](gemini://misfin.org/), and [LXMF](https://github.com/markqvist/lxmf).
@ -92,4 +105,4 @@ The smolmail project takes inspiration from [Gemini Protocol](https://geminiprot
The protocol definition, [SPEC.md](SPEC.md), is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license ([CC BY-SA-4.0](LICENSES/CC-BY-SA-4.0.txt)).
The reference code, `smolmail.py`, `smolmaild.py`, `smolmail_rns.py` and `smolmaild_rns.py`, is licensed under the Apache License 2.0 ([Apache-2.0](LICENSES/Apache-2.0.txt)).
The reference code, `smolmail.py`, `smolmaild.py`, `smolmail_rns.py`, `smolmaild_rns.py` and `vectors.py`, is licensed under the Apache License 2.0 ([Apache-2.0](LICENSES/Apache-2.0.txt)). The vectors it generates, [VECTORS.md](VECTORS.md) and [vectors.json](vectors.json), are licensed with the specification.

817
VECTORS.md Normal file
View file

@ -0,0 +1,817 @@
<!-- SPDX-License-Identifier: CC-BY-SA-4.0 -->
# smolmail Test Vectors, version 1.2
Known-answer tests for [SPEC.md](SPEC.md), generated from the reference implementation by [vectors.py](vectors.py). [vectors.json](vectors.json) carries the same values in a form a test harness can load; this file is for reading alongside the specification.
Work through them in order. Derivation first (§2), because every later vector depends on it, then an envelope you can seal and unseal (§5), then the wire frames (§4, §6.1, §7). Finish with the rejections: they are the ones that catch an implementation which decrypts without verifying.
Hex throughout, unless a field name says otherwise. The two masters are test data and the private keys here are public. Never use them for anything.
## Labels (§11)
Every hash, signature and MAC is domain-separated by one of these, prefixed to the input as raw bytes with no separator or length:
- `LABEL_IDENTITY` = `smolmail/1 identity`
- `LABEL_ACCEPT` = `smolmail/1 accept`
- `LABEL_SEAL` = `smolmail/1 seal`
- `LABEL_MSG` = `smolmail/1 msg`
- `LABEL_ID` = `smolmail/1 id`
- `LABEL_MAC` = `smolmail/1 mac`
- `LABEL_AUTH` = `smolmail/1 auth`
- `LABEL_ROTATE` = `smolmail/1 rotate`
- `LABEL_REGISTER` = `smolmail/1 register`
- `PROLOGUE` = `smolmail/1`
## §2 Identity
`seed_n = HKDF-SHA256(master, salt = "", info = "smolmail/1 identity" || n)`, with `n` a big-endian u32. The Ed25519 public key of that seed *is* the identity. The X25519 keypair comes from the same key via libsodium's conversions, and the accept key is derived without the index so it survives rotation.
### alice, rotation index 0
- `master`: `000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f`
- `rotation_index`: `0`
- `seed`: `f2fd61c1fba591a06eadf25ab3252caa1885b615f35fe9eab89dbf64ed233788`
- `identity_key`: `da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292`
- `x25519_private`: `205ef2abb8830bfaf41c910fe2fa2a67f3b7b5c7f87c74001bacbedea0be3a7a`
- `x25519_public`: `0c2dfdd8a679c9ca05c908651396b36dd4da1cf6ac3fc5b0037bde8d3feb506b`
- `base32`: `3jxsxqtdvxwobge7uxefpbordkulv6bc6ao3h4iusqlov4kgkkja`
- `fingerprint`: `3jxs xqtd vxwo bge7 uxef`
- `uri`: `smol://alice@example.org/3jxsxqtdvxwobge7uxefpbordkulv6bc6ao3h4iusqlov4kgkkja`
### alice, rotation index 1
- `master`: `000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f`
- `rotation_index`: `1`
- `seed`: `01bcae47fc0b45b9cfd8f6b6cc7ec094970e517715d9c037354646f37fd82dea`
- `identity_key`: `cb9046928dd2bede87ddc5ca42378fbd6f987e425b34a2801f74b768da39e466`
- `x25519_private`: `884e39cd42c09e5817059634c94c3855b93ebda61f864cc951c4d6e89389ea6b`
- `x25519_public`: `d9c834e4238b16e5f92bcf3fcb78fd163dec6b1c6b6ba4867f5526bed5bd6e3c`
- `base32`: `zoieneun2k7n5b65yxfeen4pxvxzq7sclm2kfaa7os3wrwrz4rta`
- `fingerprint`: `zoie neun 2k7n 5b65 yxfe`
- `uri`: `smol://alice@example.org/zoieneun2k7n5b65yxfeen4pxvxzq7sclm2kfaa7os3wrwrz4rta`
### bob, rotation index 0
- `master`: `202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f`
- `rotation_index`: `0`
- `seed`: `469fdf277869a739339c3c8d0877ca7d8bbd27170238aa61681787e5feafba8c`
- `identity_key`: `37b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c`
- `x25519_private`: `e0d577fb7fe95cf8f8e3a7d500e060155e484f89ef46c4f208fce77ccd485e48`
- `x25519_public`: `0814610bf746f1e6f33eb9275c5a8e3b43ecf92fa442f144444779d62fe1144e`
- `base32`: `g6zze6hqrpvn53gfsyleb23wgs6wmtys5vamhvuuqicfmb6ji4oa`
- `fingerprint`: `g6zz e6hq rpvn 53gf syle`
- `uri`: `smol://bob@example.org/g6zze6hqrpvn53gfsyleb23wgs6wmtys5vamhvuuqicfmb6ji4oa`
### Accept keys and tokens (§5.8)
alice's accept key, and the token she issues to bob. The accept key is derived without the rotation index, so it and every token from it survive rotation.
- `alice_accept_key`: `2c18d0eeaa9b2333f3ecd41391131ea13f10aa35903389d860c25265545026ae`
- `bob_accept_key`: `eff6fd323304674053b55a8b6a804b6a9fdbd111faad802763c1feced48259ee`
- `alice_token_for_bob`: `084e74a6dec8ed8101672225450265e5fa3dcbe3a69455569ce1ffc254df1885`
- `alice_token_for_bob_base32`: `bbhhjjw6zdwycalheisukatf4x5d3s7du2kfkvu44h74evg7dccq`
- `bob_token_for_alice_0`: `411bccb54d51808fcf329ca609e8d8dac866e556b2a90bb4374979cb0d8689b7`
- `alice_token_for_bob_rotated`: `96ddeaeabc81dadf328898de5adec032eef7d98d799fed54b8b737282090831b`
`alice_token_for_bob_rotated` is the token alice would issue to bob's index-1 key. It differs from the one bob already holds, which is why §5.8 says a token is derived from the correspondent's identity *as it stood when they were accepted*: bob's rotation does not invalidate the token he was given.
## §3 Addressing
The short form is resolved by the server; the smol:// form carries the key and needs no server to be trusted. 1961 is the default port and is omitted from the short form.
### Usernames
| username | valid |
| --- | --- |
| `a` | yes |
| `a0` | yes |
| `alice` | yes |
| `al-ice` | yes |
| `a.b-c_d` | yes |
| `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | yes |
| `(empty)` | no |
| `-alice` | no |
| `alice-` | no |
| `.alice` | no |
| `al..ice` | no |
| `a._b` | no |
| `a-_b` | no |
| `Alice` | no |
| `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | no |
| `al ice` | no |
| `alice@` | no |
| `ali/ce` | no |
### Addresses
| input | user | host | port | identity |
| --- | --- | --- | --- | --- |
| `alice@example.org` | `alice` | `example.org` | 1961 | resolved |
| `alice@example.org:1961` | `alice` | `example.org` | 1961 | resolved |
| `alice@example.org:2000` | `alice` | `example.org` | 2000 | resolved |
| `smol://alice@example.org/3jxsxqtdvxwobge7uxefpbordkulv6bc6ao3h4iusqlov4kgkkja` | `alice` | `example.org` | 1961 | `da6f2bc263adece0…` |
Refused:
- `smol://alice@example.org` — SmolError: smol://alice@example.org: smol:// address carries no key
- `smol://alice@example.org/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa` — SmolError: smol://alice@example.org/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: key is 31 bytes, expected 32
- `smol://alice@example.org/not-base32!` — SmolError: smol://alice@example.org/not-base32!: undecodable key: Non-base32 digit found
- `-alice@example.org` — SmolError: '-alice' must begin and end with a letter or digit and may not contain two separators in a row
### Fingerprint
- `identity`: `da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292`
- `encoded`: `3jxsxqtdvxwobge7uxefpbordkulv6bc6ao3h4iusqlov4kgkkja`
- `fingerprint`: `3jxs xqtd vxwo bge7 uxef`
- `fingerprint_note`: `the first 20 base32 characters in groups of four: 100 bits, so a collision costs ~2^100`
## §5 Messages
Each vector pins the ephemeral `esk` that §5.2 would draw at random and the `time` that §5.3 would take from the clock. Nothing else in the protocol is randomised, so with those two fixed an envelope reproduces byte for byte.
Build one in this order: agree on `x25519_shared`, derive `seal_key`, assemble `payload_header`, sign `signed_preimage`, concatenate the payload, then encrypt it under `aad` with an all-zero nonce. If your `seal_key` matches and your envelope does not, the fault is in the payload layout; if `x25519_shared` already differs, it is in §2's map.
`accept_mac` is what a sender holding the recipient's token attaches to `SEND` (§5.8); `message_id` is what the server returns. The full payload plaintext and the whole `SEND` request body are in vectors.json as `payload` and `send_body`.
### empty-body
body_len 0. The payload is 109 bytes and the envelope 194.
- `sender`: `bob[0]`
- `recipient`: `alice[0]`
- `body`: (empty)
- `body_len`: `0`
- `time`: `1767225600`
- `padded`: `false`
- `esk`: `4040404040404040404040404040404040404040404040404040404040404040`
- `epk`: `d7b5e81d336e578b13b8d706e82d061e3038c96bce66cdcf50d566b96ddbba10`
- `x25519_shared`: `383fc0e605a085e558481bcf53dfc0247a6f2c1ad46fe659953cf36874000469`
- `seal_key`: `d035954d97b00442b87ce8025cafe438c7791079d407dc11b75915ace169609c`
- `aad`: `534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292d7b5e81d336e578b13b8d706e82d061e3038c96bce66cdcf50d566b96ddbba10`
- `payload_header`: `0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b90000000000`
`signed_preimage`
```
736d6f6c6d61696c2f31206d7367da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1
149416eaf1465292d7b5e81d336e578b13b8d706e82d061e3038c96bce66cdcf50d566b96ddb
ba100137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000
006955b90000000000
```
- `signature`: `e41a00f14ce5dd28ca4242be7f1d4e92c4ddc5256a9071982462da20644cb136629e0c6dd817db8b0d5dcd2d23239f0b807586193acea67a3abfb339804e3501`
- `payload_len`: `109`
- `envelope_len`: `194`
`envelope`
```
534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292d7
b5e81d336e578b13b8d706e82d061e3038c96bce66cdcf50d566b96ddbba10786f5f9e9e91dd
4e29fc0a8e9516fcde856e9f6dcf5010ba87e7878bf99db9030cca7b73d61fef8a4e23e24628
f0e75b993c647361e41bd53d6ddd3a565b2ba334afaeb191f13dc801d9e7d9eeffad7eae12b0
e2e59243c7a523db3fcdb9c21e3b83508f4905624e4ae5234ba51cb5633813ffff93b60de3f1
ca8cd8a8
```
- `envelope_sha256`: `1a37555e369665f562b6a492de97a8c9cf024764b2fae32bf7bce33ebc9bc88f`
- `message_id`: `289dd78f0fb9ec8e27b1b648b1fc4cd36e388cad63e5320e8f331315998fc451`
- `accept_mac`: `1ce7f575843fa08ca8976213efbb951c8e13f119ff578da8c3f4091a15c284e5`
### unpadded
The minimal form: 69 + 45 + 11 + 64 + 16 bytes. Start here.
- `sender`: `bob[0]`
- `recipient`: `alice[0]`
- `body`: `68656c6c6f20616c696365`
- `body_len`: `11`
- `time`: `1767225600`
- `padded`: `false`
- `esk`: `4141414141414141414141414141414141414141414141414141414141414141`
- `epk`: `7a1a4e709bf085ac494aba0469b9b1eda0ab1f78b16aabb79ffeda90623e8522`
- `x25519_shared`: `012b8e969fb857162e7d00cef9e68b60ebc30a858439bf0c0e256b8807f70769`
- `seal_key`: `c4c72488dc2b6582952cb297d5d54b05b43ec7540cfdc173e55010f38d6444ed`
- `aad`: `534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf14652927a1a4e709bf085ac494aba0469b9b1eda0ab1f78b16aabb79ffeda90623e8522`
- `payload_header`: `0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b9000000000b`
`signed_preimage`
```
736d6f6c6d61696c2f31206d7367da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1
149416eaf14652927a1a4e709bf085ac494aba0469b9b1eda0ab1f78b16aabb79ffeda90623e
85220137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000
006955b9000000000b68656c6c6f20616c696365
```
- `signature`: `f4b5f898318a0c7da2f779a24972f1b51041bce5723d54e340f9887199c2f299b40b76ca3097562928cff3c2d8e02e7e78492f7f80ed6b0f784e1015b3f3b20c`
- `payload_len`: `120`
- `envelope_len`: `205`
`envelope`
```
534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf14652927a
1a4e709bf085ac494aba0469b9b1eda0ab1f78b16aabb79ffeda90623e85223f096c1be498f7
9a436bb6e96b7d425fc1173bd34c39f824e44c886a0291c1ce88d5956af3ee9d13b4c002eb50
63388392b4777356a96d32e8940477577337b48af677c773482b47c49e558e7f0dd5b93396e5
f38e3b7b01d4e2784bf4bd3df0aa84efabe12925c7522c8ca9ab867bd64aa38f1488bf2cac84
f2d6fd1f6657a67991393a5607e721
```
- `envelope_sha256`: `8594cdb21ec3373f1516253e7db829e18471c066d8a891f71e9c75d547a696c5`
- `message_id`: `75f74029f5e6a60dae846767622c09177ec75ea8de171b1b4314e44faac26d35`
- `accept_mac`: `5cb318c5db75b0f37f8b3b8bf31a5899e5f673416c0c442a3d01abc8019b01b4`
### padded
The same message with §5.3 padding to a 1024-byte multiple. A receiver must ignore the trailing zeros and trust body_len.
- `sender`: `bob[0]`
- `recipient`: `alice[0]`
- `body`: `68656c6c6f20616c696365`
- `body_len`: `11`
- `time`: `1767225600`
- `padded`: `true`
- `esk`: `4242424242424242424242424242424242424242424242424242424242424242`
- `epk`: `132c442be010fbd57e72603328aa76e71fccc1503aae219327d14d9c9993f472`
- `x25519_shared`: `2957671c03ec2f21c2d1a1c9e743564016a3107618f53077f02aff745c90ef02`
- `seal_key`: `db77b3dc494a2b299eadbbbdd3c3c0cd5e44a6b8c7ad7490729a63905668fbf4`
- `aad`: `534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292132c442be010fbd57e72603328aa76e71fccc1503aae219327d14d9c9993f472`
- `payload_header`: `0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b9000000000b`
`signed_preimage`
```
736d6f6c6d61696c2f31206d7367da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1
149416eaf1465292132c442be010fbd57e72603328aa76e71fccc1503aae219327d14d9c9993
f4720137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000
006955b9000000000b68656c6c6f20616c696365
```
- `signature`: `000e182ed5e1e10fc9ab0d1e358d54fb6ffb8fe9ea097fb795b900dfe3b9914734a11cfb0ddff7a5ce47d9b1f66fc48022b7402e2fbae0f32762632db59e7507`
- `payload_len`: `1024`
- `envelope_len`: `1109`
`envelope`
```
534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf146529213
2c442be010fbd57e72603328aa76e71fccc1503aae219327d14d9c9993f47269e94cda066826
23d97aed6f33fdd2ff38d48811450881350eb826904757f1e3e30ceeb0989c56e4638131f409
68350f507f2d61d2320abb5cc3bc433340094c37bcc350abf0e7910a672879527c99e7a9f77c
bff5e9d99138c56f75e9a98bf4128107a83176666c5db76298e338ce4be914cbecaca5ea11f1
4555cb7028ac1ccf41d0b330beada77c61eb42cab21097b005d1cf1ad60b5bfcd137a02ecd9a
af65a4692882d75e73e52d6ebcfe47bfead71ac9e54d7b1ac275aaf28a192666930324091049
3ffdd92e396387dbfff545f4d8d6b598fcb4a79f48d681f03c70e7ee378bb2cef91c9f744793
4d19aaaf36fd38434eef7dfcb2572b51ab49182278426cfb11f72614130a01907e1104ad5e27
10072041085179eac48281396bbcc5de0f2f60d89934a70b4583dda1b45188609b7d0f959559
38fb49aa2d61ad0c63d69a7f8da875b957bfff6f9b7d83a7ebb1e05f4099033e81cdaae5b0ab
83960871f3da11ceabcdd710d589948e7cd7a0e3eedee7941ad250b224d8869e657b0af53d22
ab8502919e40959acd69e303879d2ed2a4487e0d7d5a2203f886e2dd95c41655a3c4c09bcbb6
b5fbff92f1694d5e1ad5c470bbe33eaa3254e416f72d32be96d46ecaf6fc12bd891f8d3a0dc8
c36403b23f7875fa0afc518a307dcd3d7cb8846bbfdf40fedf49e3ea87a5f378050fa2f6ea8e
a86a422c58f8e7c9a077cd3fbc69c4837d748a0d32678cc7f8b482d09e3c2c9eb5037cdf2082
4be0f0c1fe26893d7e3b04a4a16ed8c7910fbf1a106a12d4c145dae793b46e36176f3c621c30
75adec0e445c033dc6d4fbe0f3c7f9419604361435e2af332cc428b038ab0e339c07248f8ac9
fc520f722558ff015b372a5d9f90a867f606b63329b21dcd05587f13e3abe40be2f5c9177048
c69168f087e071aecccff1eb3f04f95d9557dd6a40c37338765bff6de1861ca04e28a92d372e
fb92103a982d760aa89301b60c8d9a591c39492dad4467053f3c85ca226777a591434b814bdf
86d4d7557665bdf118da32e2396eba2ca3dc24731690eee9bae9398f6b9ca81f449a486ef14f
7db9c2c8e330c4b56569b24bdf1d53961d0338fd38e7ac345a1bccbd2ead3a2a133b798762bb
ddaf60f7e743b2ab0bc9f9b14b7755b02f9068ab9b4a8f154d6e9d6f61013281a6a7a69ebaa2
80f0fd7a7bc1734ddc37ef0af2d0628d5d6f7a62d345648e03b6aa84d2470a0883e79e962faa
65b3547d2b4eac386cdcdcc2afa8ebf525183854d8baafbd09691edaa73c193ad54f99bea34e
f8a2fd03ef6f869bd28bafb8377affdab5867cec7609eff5834d4cb7c9820a07fb17aebc6827
ee1c7505f820d9be04973c0b264bb3be34d54cb2e1ac4edf437a3a065a85c4158af88952721c
3d83195f898e514ba9e751060be46d2a6067e99c99a61d2959cb0d1b7ca6e76c207d26a50509
0fa3007a406471
```
- `envelope_sha256`: `41a93d239e7da918e1e6480b724b8e23eeb39ddd454b889f6a55934658c143b2`
- `message_id`: `ff849aa8b573dfc10dc5ac731b862ba1205f42c81a014744f2812d3bf1d62a2e`
- `accept_mac`: `2b0a0b0804a78a32f47e461b8ed06e6c1a21ed750b2b33290cc91f884fb86ca0`
### utf8
body_len counts bytes, not characters: 29 bytes, 22 characters.
- `sender`: `bob[0]`
- `recipient`: `alice[0]`
- `body`: `4772c3bcc39f65206175732064656d204d65736820e2809420f09f9bb00a`
- `body_len`: `30`
- `time`: `1767225600`
- `padded`: `false`
- `esk`: `4343434343434343434343434343434343434343434343434343434343434343`
- `epk`: `cdefd8783a91b446640e2e1f95599db35e484a0071bd2182b3b60d0812c10c70`
- `x25519_shared`: `40ff868360e1f61dd53a383f4e8d96205ac1eab3dea7f1c69f9a8d44f853da79`
- `seal_key`: `3bab319a2a1e80a93c2639891f9dce11c35e8cbc558fea02f3c3ef494276b54b`
- `aad`: `534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292cdefd8783a91b446640e2e1f95599db35e484a0071bd2182b3b60d0812c10c70`
- `payload_header`: `0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b9000000001e`
`signed_preimage`
```
736d6f6c6d61696c2f31206d7367da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1
149416eaf1465292cdefd8783a91b446640e2e1f95599db35e484a0071bd2182b3b60d0812c1
0c700137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000
006955b9000000001e4772c3bcc39f65206175732064656d204d65736820e2809420f09f9bb0
0a
```
- `signature`: `86372500958b7808f6fb1878d40ccc9a2b090fbb042f834b6e131ee1c7d55ef3be7ecb44db86c59bee9dc4f2268fce878e0d6e79e046969f3667bbb8b6fd110e`
- `payload_len`: `139`
- `envelope_len`: `224`
`envelope`
```
534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292cd
efd8783a91b446640e2e1f95599db35e484a0071bd2182b3b60d0812c10c70315b93e0580e4c
e845678e9aa71eeb8bf2f8c3155a8c1fd58dc43c91095e0a19032a91231f3f7ef11167dac73e
296e98c921c15c8d02e24747df1ffa600818abfb6858aa82184277da1d1fd02d1d59ada01941
8a2c708a7521e6a0f1e8e55f24f46d0bfa7d9c1ad761580f320f5c3248c18aac5158c0296992
6f427d921fee8ba4ccf2bff233733e64d5e3b50757f8f5b880d1f869651c9a186608
```
- `envelope_sha256`: `fcf78b183a0c2fad26f36b15e6cd222d1e864f48c7eea616f66aa65a14872b7c`
- `message_id`: `82c6d8c341ea821e4558dc6bb35f533e399a89646d9023437802b1d2499f2b88`
- `accept_mac`: `b221784d76eb2d52517d142912139ab0f6e61900ff35eed45998cb459683cf68`
### frontmatter
§5.5 frontmatter carrying the reserved Reply-To (§5.7) and Accept (§5.8) fields. Both are only meaningful because the signature covers them.
- `sender`: `alice[0]`
- `recipient`: `bob[0]`
`body`
```
2d2d2d0a5375626a6563743a20746865207468696e670a5265706c792d546f3a20736d6f6c3a
2f2f616c696365406578616d706c652e6f72672f336a7873787174647678776f626765377578
656670626f72646b756c7636626336616f336834697573716c6f76346b676b6b6a610a416363
6570743a20626268686a6a77367a64777963616c68656973756b617466347835643373376475
326b666b7675343468373465766737646363710a2d2d2d0a48657265206973206120746f6b65
6e20736f20796f7572206d61696c20736b697073206d7920726571756573747320746965722e
0a
```
- `body_len`: `229`
- `time`: `1767225600`
- `padded`: `false`
- `esk`: `4444444444444444444444444444444444444444444444444444444444444444`
- `epk`: `ff2ee45601ec1b67310c7790404585ae697331eee1c1f8cf2419731c1fff3e6b`
- `x25519_shared`: `07c5ae79c7568952aa78668c613389e4ecdd2f61f207618532fb3c8c8a26f213`
- `seal_key`: `4dd79e54e611ac322ac0ace7e7a64b2c2139d422097f939c603ee6dd9843f7cc`
- `aad`: `534d4f4c0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471cff2ee45601ec1b67310c7790404585ae697331eee1c1f8cf2419731c1fff3e6b`
- `payload_header`: `01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292000000006955b900000000e5`
`signed_preimage`
```
736d6f6c6d61696c2f31206d736737b39278f08beadeecc5961640eb7634bd664f12ed40c3d6
9482045607c9471cff2ee45601ec1b67310c7790404585ae697331eee1c1f8cf2419731c1fff
3e6b01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292000000
006955b900000000e52d2d2d0a5375626a6563743a20746865207468696e670a5265706c792d
546f3a20736d6f6c3a2f2f616c696365406578616d706c652e6f72672f336a78737871746476
78776f626765377578656670626f72646b756c7636626336616f336834697573716c6f76346b
676b6b6a610a4163636570743a20626268686a6a77367a64777963616c68656973756b617466
347835643373376475326b666b7675343468373465766737646363710a2d2d2d0a4865726520
6973206120746f6b656e20736f20796f7572206d61696c20736b697073206d79207265717565
73747320746965722e0a
```
- `signature`: `23e0b6bd5660ab109144e07b24affb7298041dafe908a469ab28c120f812b91dbcc7cee0763d058df4357d74ef4dee85f42da50dffdc66fb8604288a3c3d0000`
- `payload_len`: `338`
- `envelope_len`: `423`
`envelope`
```
534d4f4c0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471cff
2ee45601ec1b67310c7790404585ae697331eee1c1f8cf2419731c1fff3e6b7d16509bc2c568
3501cacd195740772232cdb04f5375e55dac3d49e258524c0eca1819d6911503a76d1b80e2e2
048947659c9925faa4e2710420d0be9110cd216f651808f4132dc13d6a91c55d9ac653b161ad
63cc70b7c9eac3b89d4994b04e791b195ca148845649a3a473f7dc26bfd0326e1f65b7559d12
824920c136e502d1c29c3211a3ca7d0782df4655e31123de9d61ebb35fe9fcfd0ecdf0c7e406
854ab7bfddbc818dd839e02ea450fd94124228a8bae41bf020962919de514525304a8f2c512b
8335bbef48af719c340d63a7e38cd6ab40c004601edf67a572062182a15328825f017d10b841
9edf162610ece5884539e8408bd6893a47b5d99d02bd944e98194424333ee1a99aabf600ec83
ae4cabc38c12c0044bc16536d54a65d78df62428b4ba290296986b19d8e143cad25ee6c20351
5c3dcb3907f3adab87ec4dd4d5cdbfe24621beb2d4901939a02df296b67c996fef0b84fc591c
87b9dc35be
```
- `envelope_sha256`: `4cd667f09da015acc5c6c1f57e57181334b89a11c2d7d543e4dcb2bf012bd698`
- `message_id`: `e6a52f744a8c0eab0c0a72c56ef51af96b83d9721d38dc669be38ca2c272f498`
- `accept_mac`: `8589379776f7d005c016593b5e04f624d7530f405213639361ab2a127004cd40`
### superseded-key
Sealed to alice's index-0 key while she is at index 1 (§7). She opens it with the retained key, which is re-derivable from the master alone.
- `sender`: `bob[0]`
- `recipient`: `alice[0]`
- `body`: `73656e74206265666f726520796f7520726f74617465640a`
- `body_len`: `24`
- `time`: `1767225600`
- `padded`: `false`
- `esk`: `4545454545454545454545454545454545454545454545454545454545454545`
- `epk`: `3286894cd2845a6db6a28fbf0677605f80e5a62385bf4e10a790ae5fde36736b`
- `x25519_shared`: `bf14188f69b809b1c944f8d46c1d4a91f641b741537b88d98622d7f2ac310434`
- `seal_key`: `7367d84d99359d719fdb9fceb49b29c19b14636dd4fc2896774061d110973cc8`
- `aad`: `534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf14652923286894cd2845a6db6a28fbf0677605f80e5a62385bf4e10a790ae5fde36736b`
- `payload_header`: `0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b90000000018`
`signed_preimage`
```
736d6f6c6d61696c2f31206d7367da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1
149416eaf14652923286894cd2845a6db6a28fbf0677605f80e5a62385bf4e10a790ae5fde36
736b0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000
006955b9000000001873656e74206265666f726520796f7520726f74617465640a
```
- `signature`: `4eb267c60e58d0f6acc2a9f0c39e384e16324ddb7bf8c23e14659682af1cfca526e5e72e39663c3d5207c929b0f65982f74ff7d3de4fce80d76bc8a39bbeac04`
- `payload_len`: `133`
- `envelope_len`: `218`
`envelope`
```
534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf146529232
86894cd2845a6db6a28fbf0677605f80e5a62385bf4e10a790ae5fde36736b4272b58c6b941f
f140f69144dc10f1d49a0f9603e7a5a97718edc2e0617631d2fed71776274b6d7d2653c1fa88
9af93c9d57fbfd0d0b0ced3f8efdf59adaa23e3101c671d55850adbbb7d776c5f1a1296b6b46
68041c0575d317b2e7bc796894091313cbf1c6be2382fe26229ba9bcfcee3281fd6b55731e06
bf72bdd73efef8b4c32c6a269a2036fc272885f0f6551f9322f727ab
```
- `envelope_sha256`: `57c33559dc31934cae4d41e99954911422178b4a54a7c7cf5a8b3174485dee37`
- `message_id`: `292854907f2bc92223edabf57ceead7276402ee3569b18d2d0d40b0e2c50886b`
- `accept_mac`: `f417c61474e4c15bbecf8586a8b1adf37a03106aea0ef818a4b970b4e8d429e9`
### rotated-key
The same correspondents after alice's rotation. Only `to` changes; her accept token does not.
- `sender`: `bob[0]`
- `recipient`: `alice[1]`
- `body`: `73656e7420616674657220796f7520726f74617465640a`
- `body_len`: `23`
- `time`: `1767225600`
- `padded`: `false`
- `esk`: `4646464646464646464646464646464646464646464646464646464646464646`
- `epk`: `a28a7c44ede257d664fbf156affa7da8abb3ae74b9fee8d7a2078543504e1a75`
- `x25519_shared`: `e8265154d3d8063d89c7b591427fab6ae3b3207bc8ba87d287c2c10bcbfb5b4a`
- `seal_key`: `b4e8a0876228c50744d1f3cde417d6299be612b485554300e44434e2f9446106`
- `aad`: `534d4f4c01cb9046928dd2bede87ddc5ca42378fbd6f987e425b34a2801f74b768da39e466a28a7c44ede257d664fbf156affa7da8abb3ae74b9fee8d7a2078543504e1a75`
- `payload_header`: `0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b90000000017`
`signed_preimage`
```
736d6f6c6d61696c2f31206d7367cb9046928dd2bede87ddc5ca42378fbd6f987e425b34a280
1f74b768da39e466a28a7c44ede257d664fbf156affa7da8abb3ae74b9fee8d7a2078543504e
1a750137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000
006955b9000000001773656e7420616674657220796f7520726f74617465640a
```
- `signature`: `857ef01e8bd0d7aee6650a86d189c9cb384c1bf6d1cc47c8666e67d88270d0d1b10a2b53388c8a9aca1c6fc0645a40cd8cb4f8af58cdc2e06801918e4e475203`
- `payload_len`: `132`
- `envelope_len`: `217`
`envelope`
```
534d4f4c01cb9046928dd2bede87ddc5ca42378fbd6f987e425b34a2801f74b768da39e466a2
8a7c44ede257d664fbf156affa7da8abb3ae74b9fee8d7a2078543504e1a75559e854a04d51f
d20336516fa2471e8b13784f6597813601f15c8f3d9aa95c049f9671ab843e1d40dc0eccf227
942b48240590da1e753e443129d8ffdaf032d7f5d2298eb7b426bd937faf52af2c0303c33e25
78574c37c0492a2b80a793d539c3ecd13a50dcd6d9ed86510749ae97e0f256d45ad8a4ea0bed
15e50a125933d17e11fc3615f288c5aeeaf706f7dbf279db165e53
```
- `envelope_sha256`: `b8fbe91f24f6f8969a7a6e2aefaf03a0f8e8e8947d8521baa9fa641c002344c4`
- `message_id`: `72c054747b8edd9c710df7ab295ed832ef791e6db4a585821968d8dff9ae3b02`
- `accept_mac`: `e7d594c129757cdbf628b354c592cf566f6b40d95fa7f6a7c6be6ca2696917cb`
## §4 and §6.1 Session frames
The AUTH signature covers the Noise handshake hash, so it cannot be replayed to another session or server. The REGISTER signature covers the server's static key for the same reason: without it, any party could bind any public key to any username.
- `handshake_hash`: `808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f`
- `server_static`: `606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f`
- `auth_signed_preimage`: `736d6f6c6d61696c2f312061757468808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f`
- `auth_signature`: `26936929ab0ead5f407d69a6516f32d2b871f41d73345b9b7d076d85eea9b6736ea0f522bcff8511c2aea1f8cf0e7a7d3f1f81f3d6d5654a27c5b81e462fa903`
`auth_frame`
```
05616c696365da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292
26936929ab0ead5f407d69a6516f32d2b871f41d73345b9b7d076d85eea9b6736ea0f522bcff
8511c2aea1f8cf0e7a7d3f1f81f3d6d5654a27c5b81e462fa903010001084e74a6dec8ed8101
672225450265e5fa3dcbe3a69455569ce1ffc254df1885
```
username_len=5 || 'alice' || identity || signature || sync=1 || count=1 || alice's token for bob
`register_signed_preimage`
```
736d6f6c6d61696c2f31207265676973746572606162636465666768696a6b6c6d6e6f707172
737475767778797a7b7c7d7e7f616c696365da6f2bc263adece0989fa5c85785d11aa8baf822
f01db3f1149416eaf1465292
```
- `register_signature`: `78364170da13b57566df066c66756c29ba4f080e8b75e3503ac7614534cff3486b888455fd5e6ad756edb4ed28567aab8ccc3820b0289d15c48720066818220f`
`register_frame`
```
05616c696365da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292
78364170da13b57566df066c66756c29ba4f080e8b75e3503ac7614534cff3486b888455fd5e
6ad756edb4ed28567aab8ccc3820b0289d15c48720066818220f0000
```
token_len=0 and cert_len=0: a first registration, no invite token and no rotation certificate
## §7 Key rotation
A certificate is 200 bytes and carries no username: the name is covered by both signatures but not transported, so a certificate cannot be replayed against another username bound to the same key.
- `username`: `alice`
- `old_key`: `da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292`
- `new_key`: `cb9046928dd2bede87ddc5ca42378fbd6f987e425b34a2801f74b768da39e466`
- `time`: `1767225600`
`signed_preimage`
```
736d6f6c6d61696c2f3120726f74617465616c696365da6f2bc263adece0989fa5c85785d11a
a8baf822f01db3f1149416eaf1465292cb9046928dd2bede87ddc5ca42378fbd6f987e425b34
a2801f74b768da39e466000000006955b900
```
- `sig_old`: `ba3bd58ab28a0bb6758bfda39b95c2e391ef922a60a010e74543c255b6689f5e81bef8ee3f84e88e61959a523429ea2d4d47622df470743caf298a19e43b9b03`
- `sig_new`: `58a928e912485548a74c2d8e9d14553a5f90c34411848a8d612fb55600539f762849102530f0ab411914e48879a62b33b1297a18ad774a614283c2268b958a0e`
`certificate`
```
da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292cb9046928dd2
bede87ddc5ca42378fbd6f987e425b34a2801f74b768da39e466000000006955b900ba3bd58a
b28a0bb6758bfda39b95c2e391ef922a60a010e74543c255b6689f5e81bef8ee3f84e88e6195
9a523429ea2d4d47622df470743caf298a19e43b9b0358a928e912485548a74c2d8e9d14553a
5f90c34411848a8d612fb55600539f762849102530f0ab411914e48879a62b33b1297a18ad77
4a614283c2268b958a0e
```
`resolve_body`
```
cb9046928dd2bede87ddc5ca42378fbd6f987e425b34a2801f74b768da39e46601da6f2bc263
adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292cb9046928dd2bede87ddc5
ca42378fbd6f987e425b34a2801f74b768da39e466000000006955b900ba3bd58ab28a0bb675
8bfda39b95c2e391ef922a60a010e74543c255b6689f5e81bef8ee3f84e88e61959a523429ea
2d4d47622df470743caf298a19e43b9b0358a928e912485548a74c2d8e9d14553a5f90c34411
848a8d612fb55600539f762849102530f0ab411914e48879a62b33b1297a18ad774a614283c2
268b958a0e
```
Chain walking:
- pinned=old, offered=new, chain=[cert], username=alice → **accept**
- same certificate checked as username=mallory → **refuse** (both signatures cover the username)
- pinned=old, offered=new, empty chain → **refuse** (a key change needs a chain; absent means ask the user)
- sig_new zeroed → **refuse** (the new key must sign too, or the old key alone could hand the username to a key nobody controls)
## §5.5 Frontmatter
A flat `Key: value` block, not YAML. A malformed line invalidates the whole block, which is then shown as ordinary body text: frontmatter fails closed toward display, never toward silent discard.
| case | input | fields | text | note |
| --- | --- | --- | --- | --- |
| `no-block` | `'hello\n'` | `{}` | `'hello\n'` | no leading '---\n', so the whole body is text |
| `simple` | `'---\nSubject: hi\n---\nbody\n'` | `{'subject': 'hi'}` | `'body\n'` | one field, then the text |
| `empty-block` | `'---\n---\n---\nliteral dashes\n'` | `{}` | `'---\nliteral dashes\n'` | an empty block escapes a body that genuinely starts with '---' |
| `case-and-duplicates` | `'---\nSubject: first\nsubject: second\nX-Mood: calm\n'…` | `{'subject': 'first', 'x-mood': 'calm'}` | `'b'` | keys are lowercased for comparison and the first occurrence wins |
| `value-trimmed` | `'---\nSubject: spaced out \n---\n'` | `{'subject': 'spaced out'}` | `''` | the value is the rest of the line, trimmed |
| `colon-in-value` | `'---\nSubject: re: the thing\n---\n'` | `{'subject': 're: the thing'}` | `''` | only the first colon separates |
| `no-colon` | `'---\nSubject hi\n---\nbody\n'` | `{}` | `'---\nSubject hi\n---\nbody\n'` | a malformed line invalidates the whole block, which becomes text |
| `bad-key` | `'---\nSub ject: hi\n---\nbody\n'` | `{}` | `'---\nSub ject: hi\n---\nbody\n'` | key charset is [A-Za-z0-9-] |
| `unterminated` | `'---\nSubject: hi\nbody\n'` | `{}` | `'---\nSubject: hi\nbody\n'` | no closing '---' |
| `too-many-keys` | `'---\nK0: v\nK1: v\nK2: v\nK3: v\nK4: v\nK5: v\nK6: v\nK7'…` | `{}` | `'---\nK0: v\nK1: v\nK2: v\nK3: v\nK4: v\nK5: v\nK6: v\nK7'…` | over 64 keys |
| `too-large` | `'---\nK: vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv'…` | `{}` | `'---\nK: vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv'…` | over 4 KiB |
| `injected-field` | `'---\nSubject: hi\nAccept: aaaaaaaa\n---\nb'` | `{'subject': 'hi', 'accept': 'aaaaaaaa'}` | `'b'` | what a line break in a sender-supplied value produces: a second field, signed and indistinguishable from an intended one. A client building frontmatter MUST refuse a value containing a line break. |
Long inputs are clipped here; vectors.json has them whole.
## Rejections
Inputs a conformant implementation refuses. The envelope cases are mutations of one valid message, listed with the reference's own error, so a client can be tested against its own failure path.
Each envelope is a mutation of this valid one:
`valid_baseline`
```
534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c3
06fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825293a846f5413d3a
945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2abdfc4
dc05c7c76b3d13e7aaee7c7d9eb6702bb90b0cbce8e59b7542200107e1fcda8f2f5462671898
30879a2d6d9493eec48c918ec35e855eb7381ee060a09b38e429c221d489cd97c23c37031b96
ad8d633e2c6b0d95f2bf
```
### bad-magic
magic is not 'SMOL' (§5.1)
`envelope`
```
584d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c3
06fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825293a846f5413d3a
945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2abdfc4
dc05c7c76b3d13e7aaee7c7d9eb6702bb90b0cbce8e59b7542200107e1fcda8f2f5462671898
30879a2d6d9493eec48c918ec35e855eb7381ee060a09b38e429c221d489cd97c23c37031b96
ad8d633e2c6b0d95f2bf
```
- `rejected_with`: `SmolError: not a smolmail envelope`
### bad-envelope-version
unknown envelope version; the sealed-box construction may differ
`envelope`
```
534d4f4c02da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c3
06fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825293a846f5413d3a
945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2abdfc4
dc05c7c76b3d13e7aaee7c7d9eb6702bb90b0cbce8e59b7542200107e1fcda8f2f5462671898
30879a2d6d9493eec48c918ec35e855eb7381ee060a09b38e429c221d489cd97c23c37031b96
ad8d633e2c6b0d95f2bf
```
- `rejected_with`: `SmolError: unsupported envelope version 2`
### truncated
shorter than the 69-byte header plus a 16-byte tag
- `envelope`: `534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c306fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d36558252`
- `rejected_with`: `SmolError: envelope too short`
### tag-flipped
the Poly1305 tag no longer authenticates
`envelope`
```
534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c3
06fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825293a846f5413d3a
945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2abdfc4
dc05c7c76b3d13e7aaee7c7d9eb6702bb90b0cbce8e59b7542200107e1fcda8f2f5462671898
30879a2d6d9493eec48c918ec35e855eb7381ee060a09b38e429c221d489cd97c23c37031b96
ad8d633e2c6b0d95f2be
```
- `rejected_with`: `SmolError: decryption failed: wrong key or corrupt envelope`
### aad-tampered
epk is covered by the AAD, so altering it breaks the tag rather than silently re-addressing the message
`envelope`
```
534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c3
06fb0ff2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825293a846f5413d3a
945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2abdfc4
dc05c7c76b3d13e7aaee7c7d9eb6702bb90b0cbce8e59b7542200107e1fcda8f2f5462671898
30879a2d6d9493eec48c918ec35e855eb7381ee060a09b38e429c221d489cd97c23c37031b96
ad8d633e2c6b0d95f2bf
```
- `rejected_with`: `SmolError: decryption failed: wrong key or corrupt envelope`
### not-for-us
`to` names a key we do not hold; a client MUST check it against its own
`envelope`
```
534d4f4c0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471cc3
06fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825293a846f5413d3a
945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2abdfc4
dc05c7c76b3d13e7aaee7c7d9eb6702bb90b0cbce8e59b7542200107e1fcda8f2f5462671898
30879a2d6d9493eec48c918ec35e855eb7381ee060a09b38e429c221d489cd97c23c37031b96
ad8d633e2c6b0d95f2bf
```
- `rejected_with`: `SmolError: addressed to g6zze6hqrpvn53gf…, not one of our keys`
### bad-signature
decrypts cleanly and the signature is zeroed. An implementation that skips §5.3 verification will accept forged mail here.
`envelope`
```
534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c3
06fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825293a846f5413d3a
945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2abdfc4
dc05c7c76b3daf229b9065119b10570cbeec2967e78dc7d26906bda10ec4702b50a7d0f93418
a97ab7aa5b6d10455d6ba65081e74824e96e1d637a4dc26845810958fe11269209adcd49b5e7
6471b2c4df881338ba42
```
- `rejected_with`: `SmolError: signature does not verify`
### signature-bound-to-other-recipient
a payload signed for bob, resealed to alice. The signature covers `to`, so re-addressing a recorded message must not verify.
`envelope`
```
534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c3
06fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825293a846f5413d3a
945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2abdfc4
dc05c7c76b3db7459221085c65f94569e41f2a7f00d84dc2045e491c300ebdfa3f8ea2af2560
1ad057d0013ef1fad9564dc1f24b034f4ebff12458d979f593fe9fa4d5a70595bf0215f69abc
1e1197d0b5979071727f
```
- `rejected_with`: `SmolError: signature does not verify`
### body-len-overflows
body_len exceeds the payload (§5.3)
`envelope`
```
534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c3
06fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825293a846f5413d3a
945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2bbdfc2
dc05c7c76b3d13e7aaee7c7d9eb6702bb90b0cbce8e59b7542200107e1fcda8f2f5462671898
30879a2d6d9493eec48c918ec35e855eb7381ee060a09b38e429c221d489cd977b2479cfcb4d
3883cf888582df8b22f0
```
- `rejected_with`: `SmolError: payload body length exceeds the payload`
### payload-version
unknown payload version
`envelope`
```
534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c3
06fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825290a846f5413d3a
945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2abdfc4
dc05c7c76b3d13e7aaee7c7d9eb6702bb90b0cbce8e59b7542200107e1fcda8f2f5462671898
30879a2d6d9493eec48c918ec35e855eb7381ee060a09b38e429c221d489cd97372cf15fd0d6
c068a477ac4c35c10862
```
- `rejected_with`: `SmolError: unsupported payload version`
### dated-in-the-future
time is 4102444800, more than 86400s ahead of any clock before 2100 (§5.3)
`envelope`
```
534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c3
06fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825293a846f5413d3a
945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd8b19bf25e2abdfc4
c605dfcb7d3da3c142dc7b0dbabf90b361744b6247bb8e9fc9407d51b383f018556d9fbfd999
beaeb49bec2d719ce3c62785f850ce261eb68784050fd68f1e7655fcae4a659fe7353abcadb4
da68e9b733c5704cbb51
```
- `rejected_with`: `SmolError: payload is dated in the future`
### low-order-epk
a low-order ephemeral key, which §2 requires be refused
`envelope`
```
534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292e0
eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b80093a846f5413d3a
945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2abdfc4
dc05c7c76b3d13e7aaee7c7d9eb6702bb90b0cbce8e59b7542200107e1fcda8f2f5462671898
30879a2d6d9493eec48c918ec35e855eb7381ee060a09b38e429c221d489cd97c23c37031b96
ad8d633e2c6b0d95f2bf
```
- `rejected_with`: `SmolError: rejected key agreement: Unexpected library error`
### Low-order X25519 keys
§2 and §4: a received ephemeral public key that is a low-order point MUST be refused, and an all-zero X25519 output MUST be refused. libsodium reports the first as the second, since the agreement output is zero.
- `0000000000000000000000000000000000000000000000000000000000000000`
- `0100000000000000000000000000000000000000000000000000000000000000`
- `e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b800`
- `5f9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f1157`
- `ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f`
- `edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f`
- `eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f`
### Invalid identity keys
Keys that do not decode as Ed25519 points, or are of small order, so §2's map to X25519 has no valid result.
- `0000000000000000000000000000000000000000000000000000000000000000` — SmolError: not a valid Ed25519 public key: Unexpected library error
- `ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff` — SmolError: not a valid Ed25519 public key: Unexpected library error
- `0100000000000000000000000000000000000000000000000000000000000000` — SmolError: not a valid Ed25519 public key: Unexpected library error
## License
These vectors are licensed under [CC BY-SA 4.0](LICENSES/CC-BY-SA-4.0.txt), with the specification.

674
vectors.json Normal file
View file

@ -0,0 +1,674 @@
{
"spec": "smolmail/1.2",
"generated_by": "vectors.py, from smolmail.py",
"note": "All values are hex unless the field name says otherwise. `alice[n]` and `bob[n]` name a master and a rotation index from `identity`. Masters are test data: never use them.",
"labels": {
"LABEL_IDENTITY": "smolmail/1 identity",
"LABEL_ACCEPT": "smolmail/1 accept",
"LABEL_SEAL": "smolmail/1 seal",
"LABEL_MSG": "smolmail/1 msg",
"LABEL_ID": "smolmail/1 id",
"LABEL_MAC": "smolmail/1 mac",
"LABEL_AUTH": "smolmail/1 auth",
"LABEL_ROTATE": "smolmail/1 rotate",
"LABEL_REGISTER": "smolmail/1 register",
"PROLOGUE": "smolmail/1"
},
"constants": {
"default_port": 1961,
"magic": "SMOL",
"version": 1,
"envelope_header": 69,
"payload_header": 45,
"pad_to": 1024,
"max_chain": 16,
"max_skew": 86400,
"noise_protocol": "Noise_NX_25519_ChaChaPoly_SHA256"
},
"identity": [
{
"name": "alice",
"master": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
"rotation_index": 0,
"seed": "f2fd61c1fba591a06eadf25ab3252caa1885b615f35fe9eab89dbf64ed233788",
"identity_key": "da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292",
"x25519_private": "205ef2abb8830bfaf41c910fe2fa2a67f3b7b5c7f87c74001bacbedea0be3a7a",
"x25519_public": "0c2dfdd8a679c9ca05c908651396b36dd4da1cf6ac3fc5b0037bde8d3feb506b",
"base32": "3jxsxqtdvxwobge7uxefpbordkulv6bc6ao3h4iusqlov4kgkkja",
"fingerprint": "3jxs xqtd vxwo bge7 uxef",
"uri": "smol://alice@example.org/3jxsxqtdvxwobge7uxefpbordkulv6bc6ao3h4iusqlov4kgkkja"
},
{
"name": "alice",
"master": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
"rotation_index": 1,
"seed": "01bcae47fc0b45b9cfd8f6b6cc7ec094970e517715d9c037354646f37fd82dea",
"identity_key": "cb9046928dd2bede87ddc5ca42378fbd6f987e425b34a2801f74b768da39e466",
"x25519_private": "884e39cd42c09e5817059634c94c3855b93ebda61f864cc951c4d6e89389ea6b",
"x25519_public": "d9c834e4238b16e5f92bcf3fcb78fd163dec6b1c6b6ba4867f5526bed5bd6e3c",
"base32": "zoieneun2k7n5b65yxfeen4pxvxzq7sclm2kfaa7os3wrwrz4rta",
"fingerprint": "zoie neun 2k7n 5b65 yxfe",
"uri": "smol://alice@example.org/zoieneun2k7n5b65yxfeen4pxvxzq7sclm2kfaa7os3wrwrz4rta"
},
{
"name": "bob",
"master": "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
"rotation_index": 0,
"seed": "469fdf277869a739339c3c8d0877ca7d8bbd27170238aa61681787e5feafba8c",
"identity_key": "37b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c",
"x25519_private": "e0d577fb7fe95cf8f8e3a7d500e060155e484f89ef46c4f208fce77ccd485e48",
"x25519_public": "0814610bf746f1e6f33eb9275c5a8e3b43ecf92fa442f144444779d62fe1144e",
"base32": "g6zze6hqrpvn53gfsyleb23wgs6wmtys5vamhvuuqicfmb6ji4oa",
"fingerprint": "g6zz e6hq rpvn 53gf syle",
"uri": "smol://bob@example.org/g6zze6hqrpvn53gfsyleb23wgs6wmtys5vamhvuuqicfmb6ji4oa"
}
],
"accept": {
"note": "alice's accept key, and the token she issues to bob. The accept key is derived without the rotation index, so it and every token from it survive rotation.",
"alice_accept_key": "2c18d0eeaa9b2333f3ecd41391131ea13f10aa35903389d860c25265545026ae",
"bob_accept_key": "eff6fd323304674053b55a8b6a804b6a9fdbd111faad802763c1feced48259ee",
"alice_token_for_bob": "084e74a6dec8ed8101672225450265e5fa3dcbe3a69455569ce1ffc254df1885",
"alice_token_for_bob_base32": "bbhhjjw6zdwycalheisukatf4x5d3s7du2kfkvu44h74evg7dccq",
"bob_token_for_alice_0": "411bccb54d51808fcf329ca609e8d8dac866e556b2a90bb4374979cb0d8689b7",
"alice_token_for_bob_rotated": "96ddeaeabc81dadf328898de5adec032eef7d98d799fed54b8b737282090831b"
},
"addressing": {
"note": "The short form is resolved by the server; the smol:// form carries the key and needs no server to be trusted. 1961 is the default port and is omitted from the short form.",
"usernames": [
{
"username": "a",
"valid": true
},
{
"username": "a0",
"valid": true
},
{
"username": "alice",
"valid": true
},
{
"username": "al-ice",
"valid": true
},
{
"username": "a.b-c_d",
"valid": true
},
{
"username": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"valid": true
},
{
"username": "",
"valid": false
},
{
"username": "-alice",
"valid": false
},
{
"username": "alice-",
"valid": false
},
{
"username": ".alice",
"valid": false
},
{
"username": "al..ice",
"valid": false
},
{
"username": "a._b",
"valid": false
},
{
"username": "a-_b",
"valid": false
},
{
"username": "Alice",
"valid": false
},
{
"username": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"valid": false
},
{
"username": "al ice",
"valid": false
},
{
"username": "alice@",
"valid": false
},
{
"username": "ali/ce",
"valid": false
}
],
"addresses": [
{
"input": "alice@example.org",
"user": "alice",
"host": "example.org",
"port": 1961,
"short": "alice@example.org",
"identity": null
},
{
"input": "alice@example.org:1961",
"user": "alice",
"host": "example.org",
"port": 1961,
"short": "alice@example.org",
"identity": null
},
{
"input": "alice@example.org:2000",
"user": "alice",
"host": "example.org",
"port": 2000,
"short": "alice@example.org:2000",
"identity": null
},
{
"input": "smol://alice@example.org/3jxsxqtdvxwobge7uxefpbordkulv6bc6ao3h4iusqlov4kgkkja",
"user": "alice",
"host": "example.org",
"port": 1961,
"short": "alice@example.org",
"identity": "da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292"
}
],
"rejected": {
"smol://alice@example.org": "SmolError: smol://alice@example.org: smol:// address carries no key",
"smol://alice@example.org/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": "SmolError: smol://alice@example.org/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: key is 31 bytes, expected 32",
"smol://alice@example.org/not-base32!": "SmolError: smol://alice@example.org/not-base32!: undecodable key: Non-base32 digit found",
"-alice@example.org": "SmolError: '-alice' must begin and end with a letter or digit and may not contain two separators in a row"
},
"base32": {
"identity": "da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292",
"encoded": "3jxsxqtdvxwobge7uxefpbordkulv6bc6ao3h4iusqlov4kgkkja",
"fingerprint": "3jxs xqtd vxwo bge7 uxef",
"fingerprint_note": "the first 20 base32 characters in groups of four: 100 bits, so a collision costs ~2^100"
}
},
"envelopes": [
{
"name": "empty-body",
"note": "body_len 0. The payload is 109 bytes and the envelope 194.",
"sender": "bob[0]",
"recipient": "alice[0]",
"body": "",
"body_len": 0,
"time": 1767225600,
"padded": false,
"esk": "4040404040404040404040404040404040404040404040404040404040404040",
"epk": "d7b5e81d336e578b13b8d706e82d061e3038c96bce66cdcf50d566b96ddbba10",
"x25519_shared": "383fc0e605a085e558481bcf53dfc0247a6f2c1ad46fe659953cf36874000469",
"seal_key": "d035954d97b00442b87ce8025cafe438c7791079d407dc11b75915ace169609c",
"aad": "534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292d7b5e81d336e578b13b8d706e82d061e3038c96bce66cdcf50d566b96ddbba10",
"payload_header": "0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b90000000000",
"signed_preimage": "736d6f6c6d61696c2f31206d7367da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292d7b5e81d336e578b13b8d706e82d061e3038c96bce66cdcf50d566b96ddbba100137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b90000000000",
"signature": "e41a00f14ce5dd28ca4242be7f1d4e92c4ddc5256a9071982462da20644cb136629e0c6dd817db8b0d5dcd2d23239f0b807586193acea67a3abfb339804e3501",
"payload": "0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b90000000000e41a00f14ce5dd28ca4242be7f1d4e92c4ddc5256a9071982462da20644cb136629e0c6dd817db8b0d5dcd2d23239f0b807586193acea67a3abfb339804e3501",
"payload_len": 109,
"envelope": "534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292d7b5e81d336e578b13b8d706e82d061e3038c96bce66cdcf50d566b96ddbba10786f5f9e9e91dd4e29fc0a8e9516fcde856e9f6dcf5010ba87e7878bf99db9030cca7b73d61fef8a4e23e24628f0e75b993c647361e41bd53d6ddd3a565b2ba334afaeb191f13dc801d9e7d9eeffad7eae12b0e2e59243c7a523db3fcdb9c21e3b83508f4905624e4ae5234ba51cb5633813ffff93b60de3f1ca8cd8a8",
"envelope_len": 194,
"envelope_sha256": "1a37555e369665f562b6a492de97a8c9cf024764b2fae32bf7bce33ebc9bc88f",
"message_id": "289dd78f0fb9ec8e27b1b648b1fc4cd36e388cad63e5320e8f331315998fc451",
"accept_mac": "1ce7f575843fa08ca8976213efbb951c8e13f119ff578da8c3f4091a15c284e5",
"send_body": "201ce7f575843fa08ca8976213efbb951c8e13f119ff578da8c3f4091a15c284e5534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292d7b5e81d336e578b13b8d706e82d061e3038c96bce66cdcf50d566b96ddbba10786f5f9e9e91dd4e29fc0a8e9516fcde856e9f6dcf5010ba87e7878bf99db9030cca7b73d61fef8a4e23e24628f0e75b993c647361e41bd53d6ddd3a565b2ba334afaeb191f13dc801d9e7d9eeffad7eae12b0e2e59243c7a523db3fcdb9c21e3b83508f4905624e4ae5234ba51cb5633813ffff93b60de3f1ca8cd8a8"
},
{
"name": "unpadded",
"note": "The minimal form: 69 + 45 + 11 + 64 + 16 bytes. Start here.",
"sender": "bob[0]",
"recipient": "alice[0]",
"body": "68656c6c6f20616c696365",
"body_len": 11,
"time": 1767225600,
"padded": false,
"esk": "4141414141414141414141414141414141414141414141414141414141414141",
"epk": "7a1a4e709bf085ac494aba0469b9b1eda0ab1f78b16aabb79ffeda90623e8522",
"x25519_shared": "012b8e969fb857162e7d00cef9e68b60ebc30a858439bf0c0e256b8807f70769",
"seal_key": "c4c72488dc2b6582952cb297d5d54b05b43ec7540cfdc173e55010f38d6444ed",
"aad": "534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf14652927a1a4e709bf085ac494aba0469b9b1eda0ab1f78b16aabb79ffeda90623e8522",
"payload_header": "0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b9000000000b",
"signed_preimage": "736d6f6c6d61696c2f31206d7367da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf14652927a1a4e709bf085ac494aba0469b9b1eda0ab1f78b16aabb79ffeda90623e85220137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b9000000000b68656c6c6f20616c696365",
"signature": "f4b5f898318a0c7da2f779a24972f1b51041bce5723d54e340f9887199c2f299b40b76ca3097562928cff3c2d8e02e7e78492f7f80ed6b0f784e1015b3f3b20c",
"payload": "0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b9000000000b68656c6c6f20616c696365f4b5f898318a0c7da2f779a24972f1b51041bce5723d54e340f9887199c2f299b40b76ca3097562928cff3c2d8e02e7e78492f7f80ed6b0f784e1015b3f3b20c",
"payload_len": 120,
"envelope": "534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf14652927a1a4e709bf085ac494aba0469b9b1eda0ab1f78b16aabb79ffeda90623e85223f096c1be498f79a436bb6e96b7d425fc1173bd34c39f824e44c886a0291c1ce88d5956af3ee9d13b4c002eb5063388392b4777356a96d32e8940477577337b48af677c773482b47c49e558e7f0dd5b93396e5f38e3b7b01d4e2784bf4bd3df0aa84efabe12925c7522c8ca9ab867bd64aa38f1488bf2cac84f2d6fd1f6657a67991393a5607e721",
"envelope_len": 205,
"envelope_sha256": "8594cdb21ec3373f1516253e7db829e18471c066d8a891f71e9c75d547a696c5",
"message_id": "75f74029f5e6a60dae846767622c09177ec75ea8de171b1b4314e44faac26d35",
"accept_mac": "5cb318c5db75b0f37f8b3b8bf31a5899e5f673416c0c442a3d01abc8019b01b4",
"send_body": "205cb318c5db75b0f37f8b3b8bf31a5899e5f673416c0c442a3d01abc8019b01b4534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf14652927a1a4e709bf085ac494aba0469b9b1eda0ab1f78b16aabb79ffeda90623e85223f096c1be498f79a436bb6e96b7d425fc1173bd34c39f824e44c886a0291c1ce88d5956af3ee9d13b4c002eb5063388392b4777356a96d32e8940477577337b48af677c773482b47c49e558e7f0dd5b93396e5f38e3b7b01d4e2784bf4bd3df0aa84efabe12925c7522c8ca9ab867bd64aa38f1488bf2cac84f2d6fd1f6657a67991393a5607e721"
},
{
"name": "padded",
"note": "The same message with \u00a75.3 padding to a 1024-byte multiple. A receiver must ignore the trailing zeros and trust body_len.",
"sender": "bob[0]",
"recipient": "alice[0]",
"body": "68656c6c6f20616c696365",
"body_len": 11,
"time": 1767225600,
"padded": true,
"esk": "4242424242424242424242424242424242424242424242424242424242424242",
"epk": "132c442be010fbd57e72603328aa76e71fccc1503aae219327d14d9c9993f472",
"x25519_shared": "2957671c03ec2f21c2d1a1c9e743564016a3107618f53077f02aff745c90ef02",
"seal_key": "db77b3dc494a2b299eadbbbdd3c3c0cd5e44a6b8c7ad7490729a63905668fbf4",
"aad": "534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292132c442be010fbd57e72603328aa76e71fccc1503aae219327d14d9c9993f472",
"payload_header": "0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b9000000000b",
"signed_preimage": "736d6f6c6d61696c2f31206d7367da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292132c442be010fbd57e72603328aa76e71fccc1503aae219327d14d9c9993f4720137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b9000000000b68656c6c6f20616c696365",
"signature": "000e182ed5e1e10fc9ab0d1e358d54fb6ffb8fe9ea097fb795b900dfe3b9914734a11cfb0ddff7a5ce47d9b1f66fc48022b7402e2fbae0f32762632db59e7507",
"payload": "0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b9000000000b68656c6c6f20616c696365000e182ed5e1e10fc9ab0d1e358d54fb6ffb8fe9ea097fb795b900dfe3b9914734a11cfb0ddff7a5ce47d9b1f66fc48022b7402e2fbae0f32762632db59e750700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"payload_len": 1024,
"envelope": "534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292132c442be010fbd57e72603328aa76e71fccc1503aae219327d14d9c9993f47269e94cda06682623d97aed6f33fdd2ff38d48811450881350eb826904757f1e3e30ceeb0989c56e4638131f40968350f507f2d61d2320abb5cc3bc433340094c37bcc350abf0e7910a672879527c99e7a9f77cbff5e9d99138c56f75e9a98bf4128107a83176666c5db76298e338ce4be914cbecaca5ea11f14555cb7028ac1ccf41d0b330beada77c61eb42cab21097b005d1cf1ad60b5bfcd137a02ecd9aaf65a4692882d75e73e52d6ebcfe47bfead71ac9e54d7b1ac275aaf28a1926669303240910493ffdd92e396387dbfff545f4d8d6b598fcb4a79f48d681f03c70e7ee378bb2cef91c9f7447934d19aaaf36fd38434eef7dfcb2572b51ab49182278426cfb11f72614130a01907e1104ad5e2710072041085179eac48281396bbcc5de0f2f60d89934a70b4583dda1b45188609b7d0f95955938fb49aa2d61ad0c63d69a7f8da875b957bfff6f9b7d83a7ebb1e05f4099033e81cdaae5b0ab83960871f3da11ceabcdd710d589948e7cd7a0e3eedee7941ad250b224d8869e657b0af53d22ab8502919e40959acd69e303879d2ed2a4487e0d7d5a2203f886e2dd95c41655a3c4c09bcbb6b5fbff92f1694d5e1ad5c470bbe33eaa3254e416f72d32be96d46ecaf6fc12bd891f8d3a0dc8c36403b23f7875fa0afc518a307dcd3d7cb8846bbfdf40fedf49e3ea87a5f378050fa2f6ea8ea86a422c58f8e7c9a077cd3fbc69c4837d748a0d32678cc7f8b482d09e3c2c9eb5037cdf20824be0f0c1fe26893d7e3b04a4a16ed8c7910fbf1a106a12d4c145dae793b46e36176f3c621c3075adec0e445c033dc6d4fbe0f3c7f9419604361435e2af332cc428b038ab0e339c07248f8ac9fc520f722558ff015b372a5d9f90a867f606b63329b21dcd05587f13e3abe40be2f5c9177048c69168f087e071aecccff1eb3f04f95d9557dd6a40c37338765bff6de1861ca04e28a92d372efb92103a982d760aa89301b60c8d9a591c39492dad4467053f3c85ca226777a591434b814bdf86d4d7557665bdf118da32e2396eba2ca3dc24731690eee9bae9398f6b9ca81f449a486ef14f7db9c2c8e330c4b56569b24bdf1d53961d0338fd38e7ac345a1bccbd2ead3a2a133b798762bbddaf60f7e743b2ab0bc9f9b14b7755b02f9068ab9b4a8f154d6e9d6f61013281a6a7a69ebaa280f0fd7a7bc1734ddc37ef0af2d0628d5d6f7a62d345648e03b6aa84d2470a0883e79e962faa65b3547d2b4eac386cdcdcc2afa8ebf525183854d8baafbd09691edaa73c193ad54f99bea34ef8a2fd03ef6f869bd28bafb8377affdab5867cec7609eff5834d4cb7c9820a07fb17aebc6827ee1c7505f820d9be04973c0b264bb3be34d54cb2e1ac4edf437a3a065a85c4158af88952721c3d83195f898e514ba9e751060be46d2a6067e99c99a61d2959cb0d1b7ca6e76c207d26a505090fa3007a406471",
"envelope_len": 1109,
"envelope_sha256": "41a93d239e7da918e1e6480b724b8e23eeb39ddd454b889f6a55934658c143b2",
"message_id": "ff849aa8b573dfc10dc5ac731b862ba1205f42c81a014744f2812d3bf1d62a2e",
"accept_mac": "2b0a0b0804a78a32f47e461b8ed06e6c1a21ed750b2b33290cc91f884fb86ca0",
"send_body": "202b0a0b0804a78a32f47e461b8ed06e6c1a21ed750b2b33290cc91f884fb86ca0534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292132c442be010fbd57e72603328aa76e71fccc1503aae219327d14d9c9993f47269e94cda06682623d97aed6f33fdd2ff38d48811450881350eb826904757f1e3e30ceeb0989c56e4638131f40968350f507f2d61d2320abb5cc3bc433340094c37bcc350abf0e7910a672879527c99e7a9f77cbff5e9d99138c56f75e9a98bf4128107a83176666c5db76298e338ce4be914cbecaca5ea11f14555cb7028ac1ccf41d0b330beada77c61eb42cab21097b005d1cf1ad60b5bfcd137a02ecd9aaf65a4692882d75e73e52d6ebcfe47bfead71ac9e54d7b1ac275aaf28a1926669303240910493ffdd92e396387dbfff545f4d8d6b598fcb4a79f48d681f03c70e7ee378bb2cef91c9f7447934d19aaaf36fd38434eef7dfcb2572b51ab49182278426cfb11f72614130a01907e1104ad5e2710072041085179eac48281396bbcc5de0f2f60d89934a70b4583dda1b45188609b7d0f95955938fb49aa2d61ad0c63d69a7f8da875b957bfff6f9b7d83a7ebb1e05f4099033e81cdaae5b0ab83960871f3da11ceabcdd710d589948e7cd7a0e3eedee7941ad250b224d8869e657b0af53d22ab8502919e40959acd69e303879d2ed2a4487e0d7d5a2203f886e2dd95c41655a3c4c09bcbb6b5fbff92f1694d5e1ad5c470bbe33eaa3254e416f72d32be96d46ecaf6fc12bd891f8d3a0dc8c36403b23f7875fa0afc518a307dcd3d7cb8846bbfdf40fedf49e3ea87a5f378050fa2f6ea8ea86a422c58f8e7c9a077cd3fbc69c4837d748a0d32678cc7f8b482d09e3c2c9eb5037cdf20824be0f0c1fe26893d7e3b04a4a16ed8c7910fbf1a106a12d4c145dae793b46e36176f3c621c3075adec0e445c033dc6d4fbe0f3c7f9419604361435e2af332cc428b038ab0e339c07248f8ac9fc520f722558ff015b372a5d9f90a867f606b63329b21dcd05587f13e3abe40be2f5c9177048c69168f087e071aecccff1eb3f04f95d9557dd6a40c37338765bff6de1861ca04e28a92d372efb92103a982d760aa89301b60c8d9a591c39492dad4467053f3c85ca226777a591434b814bdf86d4d7557665bdf118da32e2396eba2ca3dc24731690eee9bae9398f6b9ca81f449a486ef14f7db9c2c8e330c4b56569b24bdf1d53961d0338fd38e7ac345a1bccbd2ead3a2a133b798762bbddaf60f7e743b2ab0bc9f9b14b7755b02f9068ab9b4a8f154d6e9d6f61013281a6a7a69ebaa280f0fd7a7bc1734ddc37ef0af2d0628d5d6f7a62d345648e03b6aa84d2470a0883e79e962faa65b3547d2b4eac386cdcdcc2afa8ebf525183854d8baafbd09691edaa73c193ad54f99bea34ef8a2fd03ef6f869bd28bafb8377affdab5867cec7609eff5834d4cb7c9820a07fb17aebc6827ee1c7505f820d9be04973c0b264bb3be34d54cb2e1ac4edf437a3a065a85c4158af88952721c3d83195f898e514ba9e751060be46d2a6067e99c99a61d2959cb0d1b7ca6e76c207d26a505090fa3007a406471"
},
{
"name": "utf8",
"note": "body_len counts bytes, not characters: 29 bytes, 22 characters.",
"sender": "bob[0]",
"recipient": "alice[0]",
"body": "4772c3bcc39f65206175732064656d204d65736820e2809420f09f9bb00a",
"body_len": 30,
"time": 1767225600,
"padded": false,
"esk": "4343434343434343434343434343434343434343434343434343434343434343",
"epk": "cdefd8783a91b446640e2e1f95599db35e484a0071bd2182b3b60d0812c10c70",
"x25519_shared": "40ff868360e1f61dd53a383f4e8d96205ac1eab3dea7f1c69f9a8d44f853da79",
"seal_key": "3bab319a2a1e80a93c2639891f9dce11c35e8cbc558fea02f3c3ef494276b54b",
"aad": "534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292cdefd8783a91b446640e2e1f95599db35e484a0071bd2182b3b60d0812c10c70",
"payload_header": "0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b9000000001e",
"signed_preimage": "736d6f6c6d61696c2f31206d7367da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292cdefd8783a91b446640e2e1f95599db35e484a0071bd2182b3b60d0812c10c700137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b9000000001e4772c3bcc39f65206175732064656d204d65736820e2809420f09f9bb00a",
"signature": "86372500958b7808f6fb1878d40ccc9a2b090fbb042f834b6e131ee1c7d55ef3be7ecb44db86c59bee9dc4f2268fce878e0d6e79e046969f3667bbb8b6fd110e",
"payload": "0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b9000000001e4772c3bcc39f65206175732064656d204d65736820e2809420f09f9bb00a86372500958b7808f6fb1878d40ccc9a2b090fbb042f834b6e131ee1c7d55ef3be7ecb44db86c59bee9dc4f2268fce878e0d6e79e046969f3667bbb8b6fd110e",
"payload_len": 139,
"envelope": "534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292cdefd8783a91b446640e2e1f95599db35e484a0071bd2182b3b60d0812c10c70315b93e0580e4ce845678e9aa71eeb8bf2f8c3155a8c1fd58dc43c91095e0a19032a91231f3f7ef11167dac73e296e98c921c15c8d02e24747df1ffa600818abfb6858aa82184277da1d1fd02d1d59ada019418a2c708a7521e6a0f1e8e55f24f46d0bfa7d9c1ad761580f320f5c3248c18aac5158c02969926f427d921fee8ba4ccf2bff233733e64d5e3b50757f8f5b880d1f869651c9a186608",
"envelope_len": 224,
"envelope_sha256": "fcf78b183a0c2fad26f36b15e6cd222d1e864f48c7eea616f66aa65a14872b7c",
"message_id": "82c6d8c341ea821e4558dc6bb35f533e399a89646d9023437802b1d2499f2b88",
"accept_mac": "b221784d76eb2d52517d142912139ab0f6e61900ff35eed45998cb459683cf68",
"send_body": "20b221784d76eb2d52517d142912139ab0f6e61900ff35eed45998cb459683cf68534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292cdefd8783a91b446640e2e1f95599db35e484a0071bd2182b3b60d0812c10c70315b93e0580e4ce845678e9aa71eeb8bf2f8c3155a8c1fd58dc43c91095e0a19032a91231f3f7ef11167dac73e296e98c921c15c8d02e24747df1ffa600818abfb6858aa82184277da1d1fd02d1d59ada019418a2c708a7521e6a0f1e8e55f24f46d0bfa7d9c1ad761580f320f5c3248c18aac5158c02969926f427d921fee8ba4ccf2bff233733e64d5e3b50757f8f5b880d1f869651c9a186608"
},
{
"name": "frontmatter",
"note": "\u00a75.5 frontmatter carrying the reserved Reply-To (\u00a75.7) and Accept (\u00a75.8) fields. Both are only meaningful because the signature covers them.",
"sender": "alice[0]",
"recipient": "bob[0]",
"body": "2d2d2d0a5375626a6563743a20746865207468696e670a5265706c792d546f3a20736d6f6c3a2f2f616c696365406578616d706c652e6f72672f336a7873787174647678776f626765377578656670626f72646b756c7636626336616f336834697573716c6f76346b676b6b6a610a4163636570743a20626268686a6a77367a64777963616c68656973756b617466347835643373376475326b666b7675343468373465766737646363710a2d2d2d0a48657265206973206120746f6b656e20736f20796f7572206d61696c20736b697073206d7920726571756573747320746965722e0a",
"body_len": 229,
"time": 1767225600,
"padded": false,
"esk": "4444444444444444444444444444444444444444444444444444444444444444",
"epk": "ff2ee45601ec1b67310c7790404585ae697331eee1c1f8cf2419731c1fff3e6b",
"x25519_shared": "07c5ae79c7568952aa78668c613389e4ecdd2f61f207618532fb3c8c8a26f213",
"seal_key": "4dd79e54e611ac322ac0ace7e7a64b2c2139d422097f939c603ee6dd9843f7cc",
"aad": "534d4f4c0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471cff2ee45601ec1b67310c7790404585ae697331eee1c1f8cf2419731c1fff3e6b",
"payload_header": "01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292000000006955b900000000e5",
"signed_preimage": "736d6f6c6d61696c2f31206d736737b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471cff2ee45601ec1b67310c7790404585ae697331eee1c1f8cf2419731c1fff3e6b01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292000000006955b900000000e52d2d2d0a5375626a6563743a20746865207468696e670a5265706c792d546f3a20736d6f6c3a2f2f616c696365406578616d706c652e6f72672f336a7873787174647678776f626765377578656670626f72646b756c7636626336616f336834697573716c6f76346b676b6b6a610a4163636570743a20626268686a6a77367a64777963616c68656973756b617466347835643373376475326b666b7675343468373465766737646363710a2d2d2d0a48657265206973206120746f6b656e20736f20796f7572206d61696c20736b697073206d7920726571756573747320746965722e0a",
"signature": "23e0b6bd5660ab109144e07b24affb7298041dafe908a469ab28c120f812b91dbcc7cee0763d058df4357d74ef4dee85f42da50dffdc66fb8604288a3c3d0000",
"payload": "01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292000000006955b900000000e52d2d2d0a5375626a6563743a20746865207468696e670a5265706c792d546f3a20736d6f6c3a2f2f616c696365406578616d706c652e6f72672f336a7873787174647678776f626765377578656670626f72646b756c7636626336616f336834697573716c6f76346b676b6b6a610a4163636570743a20626268686a6a77367a64777963616c68656973756b617466347835643373376475326b666b7675343468373465766737646363710a2d2d2d0a48657265206973206120746f6b656e20736f20796f7572206d61696c20736b697073206d7920726571756573747320746965722e0a23e0b6bd5660ab109144e07b24affb7298041dafe908a469ab28c120f812b91dbcc7cee0763d058df4357d74ef4dee85f42da50dffdc66fb8604288a3c3d0000",
"payload_len": 338,
"envelope": "534d4f4c0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471cff2ee45601ec1b67310c7790404585ae697331eee1c1f8cf2419731c1fff3e6b7d16509bc2c5683501cacd195740772232cdb04f5375e55dac3d49e258524c0eca1819d6911503a76d1b80e2e2048947659c9925faa4e2710420d0be9110cd216f651808f4132dc13d6a91c55d9ac653b161ad63cc70b7c9eac3b89d4994b04e791b195ca148845649a3a473f7dc26bfd0326e1f65b7559d12824920c136e502d1c29c3211a3ca7d0782df4655e31123de9d61ebb35fe9fcfd0ecdf0c7e406854ab7bfddbc818dd839e02ea450fd94124228a8bae41bf020962919de514525304a8f2c512b8335bbef48af719c340d63a7e38cd6ab40c004601edf67a572062182a15328825f017d10b8419edf162610ece5884539e8408bd6893a47b5d99d02bd944e98194424333ee1a99aabf600ec83ae4cabc38c12c0044bc16536d54a65d78df62428b4ba290296986b19d8e143cad25ee6c203515c3dcb3907f3adab87ec4dd4d5cdbfe24621beb2d4901939a02df296b67c996fef0b84fc591c87b9dc35be",
"envelope_len": 423,
"envelope_sha256": "4cd667f09da015acc5c6c1f57e57181334b89a11c2d7d543e4dcb2bf012bd698",
"message_id": "e6a52f744a8c0eab0c0a72c56ef51af96b83d9721d38dc669be38ca2c272f498",
"accept_mac": "8589379776f7d005c016593b5e04f624d7530f405213639361ab2a127004cd40",
"send_body": "208589379776f7d005c016593b5e04f624d7530f405213639361ab2a127004cd40534d4f4c0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471cff2ee45601ec1b67310c7790404585ae697331eee1c1f8cf2419731c1fff3e6b7d16509bc2c5683501cacd195740772232cdb04f5375e55dac3d49e258524c0eca1819d6911503a76d1b80e2e2048947659c9925faa4e2710420d0be9110cd216f651808f4132dc13d6a91c55d9ac653b161ad63cc70b7c9eac3b89d4994b04e791b195ca148845649a3a473f7dc26bfd0326e1f65b7559d12824920c136e502d1c29c3211a3ca7d0782df4655e31123de9d61ebb35fe9fcfd0ecdf0c7e406854ab7bfddbc818dd839e02ea450fd94124228a8bae41bf020962919de514525304a8f2c512b8335bbef48af719c340d63a7e38cd6ab40c004601edf67a572062182a15328825f017d10b8419edf162610ece5884539e8408bd6893a47b5d99d02bd944e98194424333ee1a99aabf600ec83ae4cabc38c12c0044bc16536d54a65d78df62428b4ba290296986b19d8e143cad25ee6c203515c3dcb3907f3adab87ec4dd4d5cdbfe24621beb2d4901939a02df296b67c996fef0b84fc591c87b9dc35be"
},
{
"name": "superseded-key",
"note": "Sealed to alice's index-0 key while she is at index 1 (\u00a77). She opens it with the retained key, which is re-derivable from the master alone.",
"sender": "bob[0]",
"recipient": "alice[0]",
"body": "73656e74206265666f726520796f7520726f74617465640a",
"body_len": 24,
"time": 1767225600,
"padded": false,
"esk": "4545454545454545454545454545454545454545454545454545454545454545",
"epk": "3286894cd2845a6db6a28fbf0677605f80e5a62385bf4e10a790ae5fde36736b",
"x25519_shared": "bf14188f69b809b1c944f8d46c1d4a91f641b741537b88d98622d7f2ac310434",
"seal_key": "7367d84d99359d719fdb9fceb49b29c19b14636dd4fc2896774061d110973cc8",
"aad": "534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf14652923286894cd2845a6db6a28fbf0677605f80e5a62385bf4e10a790ae5fde36736b",
"payload_header": "0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b90000000018",
"signed_preimage": "736d6f6c6d61696c2f31206d7367da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf14652923286894cd2845a6db6a28fbf0677605f80e5a62385bf4e10a790ae5fde36736b0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b9000000001873656e74206265666f726520796f7520726f74617465640a",
"signature": "4eb267c60e58d0f6acc2a9f0c39e384e16324ddb7bf8c23e14659682af1cfca526e5e72e39663c3d5207c929b0f65982f74ff7d3de4fce80d76bc8a39bbeac04",
"payload": "0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b9000000001873656e74206265666f726520796f7520726f74617465640a4eb267c60e58d0f6acc2a9f0c39e384e16324ddb7bf8c23e14659682af1cfca526e5e72e39663c3d5207c929b0f65982f74ff7d3de4fce80d76bc8a39bbeac04",
"payload_len": 133,
"envelope": "534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf14652923286894cd2845a6db6a28fbf0677605f80e5a62385bf4e10a790ae5fde36736b4272b58c6b941ff140f69144dc10f1d49a0f9603e7a5a97718edc2e0617631d2fed71776274b6d7d2653c1fa889af93c9d57fbfd0d0b0ced3f8efdf59adaa23e3101c671d55850adbbb7d776c5f1a1296b6b4668041c0575d317b2e7bc796894091313cbf1c6be2382fe26229ba9bcfcee3281fd6b55731e06bf72bdd73efef8b4c32c6a269a2036fc272885f0f6551f9322f727ab",
"envelope_len": 218,
"envelope_sha256": "57c33559dc31934cae4d41e99954911422178b4a54a7c7cf5a8b3174485dee37",
"message_id": "292854907f2bc92223edabf57ceead7276402ee3569b18d2d0d40b0e2c50886b",
"accept_mac": "f417c61474e4c15bbecf8586a8b1adf37a03106aea0ef818a4b970b4e8d429e9",
"send_body": "20f417c61474e4c15bbecf8586a8b1adf37a03106aea0ef818a4b970b4e8d429e9534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf14652923286894cd2845a6db6a28fbf0677605f80e5a62385bf4e10a790ae5fde36736b4272b58c6b941ff140f69144dc10f1d49a0f9603e7a5a97718edc2e0617631d2fed71776274b6d7d2653c1fa889af93c9d57fbfd0d0b0ced3f8efdf59adaa23e3101c671d55850adbbb7d776c5f1a1296b6b4668041c0575d317b2e7bc796894091313cbf1c6be2382fe26229ba9bcfcee3281fd6b55731e06bf72bdd73efef8b4c32c6a269a2036fc272885f0f6551f9322f727ab"
},
{
"name": "rotated-key",
"note": "The same correspondents after alice's rotation. Only `to` changes; her accept token does not.",
"sender": "bob[0]",
"recipient": "alice[1]",
"body": "73656e7420616674657220796f7520726f74617465640a",
"body_len": 23,
"time": 1767225600,
"padded": false,
"esk": "4646464646464646464646464646464646464646464646464646464646464646",
"epk": "a28a7c44ede257d664fbf156affa7da8abb3ae74b9fee8d7a2078543504e1a75",
"x25519_shared": "e8265154d3d8063d89c7b591427fab6ae3b3207bc8ba87d287c2c10bcbfb5b4a",
"seal_key": "b4e8a0876228c50744d1f3cde417d6299be612b485554300e44434e2f9446106",
"aad": "534d4f4c01cb9046928dd2bede87ddc5ca42378fbd6f987e425b34a2801f74b768da39e466a28a7c44ede257d664fbf156affa7da8abb3ae74b9fee8d7a2078543504e1a75",
"payload_header": "0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b90000000017",
"signed_preimage": "736d6f6c6d61696c2f31206d7367cb9046928dd2bede87ddc5ca42378fbd6f987e425b34a2801f74b768da39e466a28a7c44ede257d664fbf156affa7da8abb3ae74b9fee8d7a2078543504e1a750137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b9000000001773656e7420616674657220796f7520726f74617465640a",
"signature": "857ef01e8bd0d7aee6650a86d189c9cb384c1bf6d1cc47c8666e67d88270d0d1b10a2b53388c8a9aca1c6fc0645a40cd8cb4f8af58cdc2e06801918e4e475203",
"payload": "0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471c000000006955b9000000001773656e7420616674657220796f7520726f74617465640a857ef01e8bd0d7aee6650a86d189c9cb384c1bf6d1cc47c8666e67d88270d0d1b10a2b53388c8a9aca1c6fc0645a40cd8cb4f8af58cdc2e06801918e4e475203",
"payload_len": 132,
"envelope": "534d4f4c01cb9046928dd2bede87ddc5ca42378fbd6f987e425b34a2801f74b768da39e466a28a7c44ede257d664fbf156affa7da8abb3ae74b9fee8d7a2078543504e1a75559e854a04d51fd20336516fa2471e8b13784f6597813601f15c8f3d9aa95c049f9671ab843e1d40dc0eccf227942b48240590da1e753e443129d8ffdaf032d7f5d2298eb7b426bd937faf52af2c0303c33e2578574c37c0492a2b80a793d539c3ecd13a50dcd6d9ed86510749ae97e0f256d45ad8a4ea0bed15e50a125933d17e11fc3615f288c5aeeaf706f7dbf279db165e53",
"envelope_len": 217,
"envelope_sha256": "b8fbe91f24f6f8969a7a6e2aefaf03a0f8e8e8947d8521baa9fa641c002344c4",
"message_id": "72c054747b8edd9c710df7ab295ed832ef791e6db4a585821968d8dff9ae3b02",
"accept_mac": "e7d594c129757cdbf628b354c592cf566f6b40d95fa7f6a7c6be6ca2696917cb",
"send_body": "20e7d594c129757cdbf628b354c592cf566f6b40d95fa7f6a7c6be6ca2696917cb534d4f4c01cb9046928dd2bede87ddc5ca42378fbd6f987e425b34a2801f74b768da39e466a28a7c44ede257d664fbf156affa7da8abb3ae74b9fee8d7a2078543504e1a75559e854a04d51fd20336516fa2471e8b13784f6597813601f15c8f3d9aa95c049f9671ab843e1d40dc0eccf227942b48240590da1e753e443129d8ffdaf032d7f5d2298eb7b426bd937faf52af2c0303c33e2578574c37c0492a2b80a793d539c3ecd13a50dcd6d9ed86510749ae97e0f256d45ad8a4ea0bed15e50a125933d17e11fc3615f288c5aeeaf706f7dbf279db165e53"
}
],
"session": {
"note": "The AUTH signature covers the Noise handshake hash, so it cannot be replayed to another session or server. The REGISTER signature covers the server's static key for the same reason: without it, any party could bind any public key to any username.",
"handshake_hash": "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f",
"server_static": "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f",
"auth_signed_preimage": "736d6f6c6d61696c2f312061757468808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f",
"auth_signature": "26936929ab0ead5f407d69a6516f32d2b871f41d73345b9b7d076d85eea9b6736ea0f522bcff8511c2aea1f8cf0e7a7d3f1f81f3d6d5654a27c5b81e462fa903",
"auth_frame": "05616c696365da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf146529226936929ab0ead5f407d69a6516f32d2b871f41d73345b9b7d076d85eea9b6736ea0f522bcff8511c2aea1f8cf0e7a7d3f1f81f3d6d5654a27c5b81e462fa903010001084e74a6dec8ed8101672225450265e5fa3dcbe3a69455569ce1ffc254df1885",
"auth_frame_note": "username_len=5 || 'alice' || identity || signature || sync=1 || count=1 || alice's token for bob",
"register_signed_preimage": "736d6f6c6d61696c2f31207265676973746572606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f616c696365da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292",
"register_signature": "78364170da13b57566df066c66756c29ba4f080e8b75e3503ac7614534cff3486b888455fd5e6ad756edb4ed28567aab8ccc3820b0289d15c48720066818220f",
"register_frame": "05616c696365da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf146529278364170da13b57566df066c66756c29ba4f080e8b75e3503ac7614534cff3486b888455fd5e6ad756edb4ed28567aab8ccc3820b0289d15c48720066818220f0000",
"register_frame_note": "token_len=0 and cert_len=0: a first registration, no invite token and no rotation certificate"
},
"rotation": {
"note": "A certificate is 200 bytes and carries no username: the name is covered by both signatures but not transported, so a certificate cannot be replayed against another username bound to the same key.",
"username": "alice",
"old_key": "da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292",
"new_key": "cb9046928dd2bede87ddc5ca42378fbd6f987e425b34a2801f74b768da39e466",
"time": 1767225600,
"signed_preimage": "736d6f6c6d61696c2f3120726f74617465616c696365da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292cb9046928dd2bede87ddc5ca42378fbd6f987e425b34a2801f74b768da39e466000000006955b900",
"sig_old": "ba3bd58ab28a0bb6758bfda39b95c2e391ef922a60a010e74543c255b6689f5e81bef8ee3f84e88e61959a523429ea2d4d47622df470743caf298a19e43b9b03",
"sig_new": "58a928e912485548a74c2d8e9d14553a5f90c34411848a8d612fb55600539f762849102530f0ab411914e48879a62b33b1297a18ad774a614283c2268b958a0e",
"certificate": "da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292cb9046928dd2bede87ddc5ca42378fbd6f987e425b34a2801f74b768da39e466000000006955b900ba3bd58ab28a0bb6758bfda39b95c2e391ef922a60a010e74543c255b6689f5e81bef8ee3f84e88e61959a523429ea2d4d47622df470743caf298a19e43b9b0358a928e912485548a74c2d8e9d14553a5f90c34411848a8d612fb55600539f762849102530f0ab411914e48879a62b33b1297a18ad774a614283c2268b958a0e",
"resolve_body": "cb9046928dd2bede87ddc5ca42378fbd6f987e425b34a2801f74b768da39e46601da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292cb9046928dd2bede87ddc5ca42378fbd6f987e425b34a2801f74b768da39e466000000006955b900ba3bd58ab28a0bb6758bfda39b95c2e391ef922a60a010e74543c255b6689f5e81bef8ee3f84e88e61959a523429ea2d4d47622df470743caf298a19e43b9b0358a928e912485548a74c2d8e9d14553a5f90c34411848a8d612fb55600539f762849102530f0ab411914e48879a62b33b1297a18ad774a614283c2268b958a0e",
"walk_chain": [
{
"case": "pinned=old, offered=new, chain=[cert], username=alice",
"accepted": true
},
{
"case": "same certificate checked as username=mallory",
"accepted": false,
"why": "both signatures cover the username"
},
{
"case": "pinned=old, offered=new, empty chain",
"accepted": false,
"why": "a key change needs a chain; absent means ask the user"
},
{
"case": "sig_new zeroed",
"accepted": false,
"why": "the new key must sign too, or the old key alone could hand the username to a key nobody controls"
}
]
},
"frontmatter": [
{
"name": "no-block",
"note": "no leading '---\\n', so the whole body is text",
"body": "hello\n",
"fields": {},
"text": "hello\n",
"block_accepted": false
},
{
"name": "simple",
"note": "one field, then the text",
"body": "---\nSubject: hi\n---\nbody\n",
"fields": {
"subject": "hi"
},
"text": "body\n",
"block_accepted": true
},
{
"name": "empty-block",
"note": "an empty block escapes a body that genuinely starts with '---'",
"body": "---\n---\n---\nliteral dashes\n",
"fields": {},
"text": "---\nliteral dashes\n",
"block_accepted": true
},
{
"name": "case-and-duplicates",
"note": "keys are lowercased for comparison and the first occurrence wins",
"body": "---\nSubject: first\nsubject: second\nX-Mood: calm\n---\nb",
"fields": {
"subject": "first",
"x-mood": "calm"
},
"text": "b",
"block_accepted": true
},
{
"name": "value-trimmed",
"note": "the value is the rest of the line, trimmed",
"body": "---\nSubject: spaced out \n---\n",
"fields": {
"subject": "spaced out"
},
"text": "",
"block_accepted": true
},
{
"name": "colon-in-value",
"note": "only the first colon separates",
"body": "---\nSubject: re: the thing\n---\n",
"fields": {
"subject": "re: the thing"
},
"text": "",
"block_accepted": true
},
{
"name": "no-colon",
"note": "a malformed line invalidates the whole block, which becomes text",
"body": "---\nSubject hi\n---\nbody\n",
"fields": {},
"text": "---\nSubject hi\n---\nbody\n",
"block_accepted": false
},
{
"name": "bad-key",
"note": "key charset is [A-Za-z0-9-]",
"body": "---\nSub ject: hi\n---\nbody\n",
"fields": {},
"text": "---\nSub ject: hi\n---\nbody\n",
"block_accepted": false
},
{
"name": "unterminated",
"note": "no closing '---'",
"body": "---\nSubject: hi\nbody\n",
"fields": {},
"text": "---\nSubject: hi\nbody\n",
"block_accepted": false
},
{
"name": "too-many-keys",
"note": "over 64 keys",
"body": "---\nK0: v\nK1: v\nK2: v\nK3: v\nK4: v\nK5: v\nK6: v\nK7: v\nK8: v\nK9: v\nK10: v\nK11: v\nK12: v\nK13: v\nK14: v\nK15: v\nK16: v\nK17: v\nK18: v\nK19: v\nK20: v\nK21: v\nK22: v\nK23: v\nK24: v\nK25: v\nK26: v\nK27: v\nK28: v\nK29: v\nK30: v\nK31: v\nK32: v\nK33: v\nK34: v\nK35: v\nK36: v\nK37: v\nK38: v\nK39: v\nK40: v\nK41: v\nK42: v\nK43: v\nK44: v\nK45: v\nK46: v\nK47: v\nK48: v\nK49: v\nK50: v\nK51: v\nK52: v\nK53: v\nK54: v\nK55: v\nK56: v\nK57: v\nK58: v\nK59: v\nK60: v\nK61: v\nK62: v\nK63: v\nK64: v\n---\nb",
"fields": {},
"text": "---\nK0: v\nK1: v\nK2: v\nK3: v\nK4: v\nK5: v\nK6: v\nK7: v\nK8: v\nK9: v\nK10: v\nK11: v\nK12: v\nK13: v\nK14: v\nK15: v\nK16: v\nK17: v\nK18: v\nK19: v\nK20: v\nK21: v\nK22: v\nK23: v\nK24: v\nK25: v\nK26: v\nK27: v\nK28: v\nK29: v\nK30: v\nK31: v\nK32: v\nK33: v\nK34: v\nK35: v\nK36: v\nK37: v\nK38: v\nK39: v\nK40: v\nK41: v\nK42: v\nK43: v\nK44: v\nK45: v\nK46: v\nK47: v\nK48: v\nK49: v\nK50: v\nK51: v\nK52: v\nK53: v\nK54: v\nK55: v\nK56: v\nK57: v\nK58: v\nK59: v\nK60: v\nK61: v\nK62: v\nK63: v\nK64: v\n---\nb",
"block_accepted": false
},
{
"name": "too-large",
"note": "over 4 KiB",
"body": "---\nK: vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n---\nb",
"fields": {},
"text": "---\nK: vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n---\nb",
"block_accepted": false
},
{
"name": "injected-field",
"note": "what a line break in a sender-supplied value produces: a second field, signed and indistinguishable from an intended one. A client building frontmatter MUST refuse a value containing a line break.",
"body": "---\nSubject: hi\nAccept: aaaaaaaa\n---\nb",
"fields": {
"subject": "hi",
"accept": "aaaaaaaa"
},
"text": "b",
"block_accepted": true
}
],
"rejects": {
"note": "Inputs a conformant implementation refuses. The envelope cases are mutations of one valid message, listed with the reference's own error, so a client can be tested against its own failure path.",
"valid_baseline": "534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c306fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825293a846f5413d3a945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2abdfc4dc05c7c76b3d13e7aaee7c7d9eb6702bb90b0cbce8e59b7542200107e1fcda8f2f546267189830879a2d6d9493eec48c918ec35e855eb7381ee060a09b38e429c221d489cd97c23c37031b96ad8d633e2c6b0d95f2bf",
"envelopes": [
{
"name": "bad-magic",
"why": "magic is not 'SMOL' (\u00a75.1)",
"envelope": "584d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c306fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825293a846f5413d3a945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2abdfc4dc05c7c76b3d13e7aaee7c7d9eb6702bb90b0cbce8e59b7542200107e1fcda8f2f546267189830879a2d6d9493eec48c918ec35e855eb7381ee060a09b38e429c221d489cd97c23c37031b96ad8d633e2c6b0d95f2bf",
"envelope_len": 200,
"rejected_with": "SmolError: not a smolmail envelope"
},
{
"name": "bad-envelope-version",
"why": "unknown envelope version; the sealed-box construction may differ",
"envelope": "534d4f4c02da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c306fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825293a846f5413d3a945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2abdfc4dc05c7c76b3d13e7aaee7c7d9eb6702bb90b0cbce8e59b7542200107e1fcda8f2f546267189830879a2d6d9493eec48c918ec35e855eb7381ee060a09b38e429c221d489cd97c23c37031b96ad8d633e2c6b0d95f2bf",
"envelope_len": 200,
"rejected_with": "SmolError: unsupported envelope version 2"
},
{
"name": "truncated",
"why": "shorter than the 69-byte header plus a 16-byte tag",
"envelope": "534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c306fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d36558252",
"envelope_len": 69,
"rejected_with": "SmolError: envelope too short"
},
{
"name": "tag-flipped",
"why": "the Poly1305 tag no longer authenticates",
"envelope": "534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c306fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825293a846f5413d3a945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2abdfc4dc05c7c76b3d13e7aaee7c7d9eb6702bb90b0cbce8e59b7542200107e1fcda8f2f546267189830879a2d6d9493eec48c918ec35e855eb7381ee060a09b38e429c221d489cd97c23c37031b96ad8d633e2c6b0d95f2be",
"envelope_len": 200,
"rejected_with": "SmolError: decryption failed: wrong key or corrupt envelope"
},
{
"name": "aad-tampered",
"why": "epk is covered by the AAD, so altering it breaks the tag rather than silently re-addressing the message",
"envelope": "534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c306fb0ff2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825293a846f5413d3a945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2abdfc4dc05c7c76b3d13e7aaee7c7d9eb6702bb90b0cbce8e59b7542200107e1fcda8f2f546267189830879a2d6d9493eec48c918ec35e855eb7381ee060a09b38e429c221d489cd97c23c37031b96ad8d633e2c6b0d95f2bf",
"envelope_len": 200,
"rejected_with": "SmolError: decryption failed: wrong key or corrupt envelope"
},
{
"name": "not-for-us",
"why": "`to` names a key we do not hold; a client MUST check it against its own",
"envelope": "534d4f4c0137b39278f08beadeecc5961640eb7634bd664f12ed40c3d69482045607c9471cc306fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825293a846f5413d3a945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2abdfc4dc05c7c76b3d13e7aaee7c7d9eb6702bb90b0cbce8e59b7542200107e1fcda8f2f546267189830879a2d6d9493eec48c918ec35e855eb7381ee060a09b38e429c221d489cd97c23c37031b96ad8d633e2c6b0d95f2bf",
"envelope_len": 200,
"rejected_with": "SmolError: addressed to g6zze6hqrpvn53gf\u2026, not one of our keys"
},
{
"name": "bad-signature",
"why": "decrypts cleanly and the signature is zeroed. An implementation that skips \u00a75.3 verification will accept forged mail here.",
"envelope": "534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c306fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825293a846f5413d3a945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2abdfc4dc05c7c76b3daf229b9065119b10570cbeec2967e78dc7d26906bda10ec4702b50a7d0f93418a97ab7aa5b6d10455d6ba65081e74824e96e1d637a4dc26845810958fe11269209adcd49b5e76471b2c4df881338ba42",
"envelope_len": 200,
"rejected_with": "SmolError: signature does not verify"
},
{
"name": "signature-bound-to-other-recipient",
"why": "a payload signed for bob, resealed to alice. The signature covers `to`, so re-addressing a recorded message must not verify.",
"envelope": "534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c306fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825293a846f5413d3a945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2abdfc4dc05c7c76b3db7459221085c65f94569e41f2a7f00d84dc2045e491c300ebdfa3f8ea2af25601ad057d0013ef1fad9564dc1f24b034f4ebff12458d979f593fe9fa4d5a70595bf0215f69abc1e1197d0b5979071727f",
"envelope_len": 200,
"rejected_with": "SmolError: signature does not verify"
},
{
"name": "body-len-overflows",
"why": "body_len exceeds the payload (\u00a75.3)",
"envelope": "534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c306fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825293a846f5413d3a945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2bbdfc2dc05c7c76b3d13e7aaee7c7d9eb6702bb90b0cbce8e59b7542200107e1fcda8f2f546267189830879a2d6d9493eec48c918ec35e855eb7381ee060a09b38e429c221d489cd977b2479cfcb4d3883cf888582df8b22f0",
"envelope_len": 200,
"rejected_with": "SmolError: payload body length exceeds the payload"
},
{
"name": "payload-version",
"why": "unknown payload version",
"envelope": "534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c306fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825290a846f5413d3a945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2abdfc4dc05c7c76b3d13e7aaee7c7d9eb6702bb90b0cbce8e59b7542200107e1fcda8f2f546267189830879a2d6d9493eec48c918ec35e855eb7381ee060a09b38e429c221d489cd97372cf15fd0d6c068a477ac4c35c10862",
"envelope_len": 200,
"rejected_with": "SmolError: unsupported payload version"
},
{
"name": "dated-in-the-future",
"why": "time is 4102444800, more than 86400s ahead of any clock before 2100 (\u00a75.3)",
"envelope": "534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292c306fb0ef2bf8b7f93bad98155fa37daec74db0c4cbeda6c6f1dba9d3655825293a846f5413d3a945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd8b19bf25e2abdfc4c605dfcb7d3da3c142dc7b0dbabf90b361744b6247bb8e9fc9407d51b383f018556d9fbfd999beaeb49bec2d719ce3c62785f850ce261eb68784050fd68f1e7655fcae4a659fe7353abcadb4da68e9b733c5704cbb51",
"envelope_len": 200,
"rejected_with": "SmolError: payload is dated in the future"
},
{
"name": "low-order-epk",
"why": "a low-order ephemeral key, which \u00a72 requires be refused",
"envelope": "534d4f4c01da6f2bc263adece0989fa5c85785d11aa8baf822f01db3f1149416eaf1465292e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b80093a846f5413d3a945f8aa55bdc4f0fffd51e4f59624b753b1691571500e2db82aa003ffedd16ca5125e2abdfc4dc05c7c76b3d13e7aaee7c7d9eb6702bb90b0cbce8e59b7542200107e1fcda8f2f546267189830879a2d6d9493eec48c918ec35e855eb7381ee060a09b38e429c221d489cd97c23c37031b96ad8d633e2c6b0d95f2bf",
"envelope_len": 200,
"rejected_with": "SmolError: rejected key agreement: Unexpected library error"
}
],
"low_order_x25519": [
{
"x25519_public": "0000000000000000000000000000000000000000000000000000000000000000",
"rejected_with": "SmolError: rejected key agreement: Unexpected library error"
},
{
"x25519_public": "0100000000000000000000000000000000000000000000000000000000000000",
"rejected_with": "SmolError: rejected key agreement: Unexpected library error"
},
{
"x25519_public": "e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b800",
"rejected_with": "SmolError: rejected key agreement: Unexpected library error"
},
{
"x25519_public": "5f9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f1157",
"rejected_with": "SmolError: rejected key agreement: Unexpected library error"
},
{
"x25519_public": "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
"rejected_with": "SmolError: rejected key agreement: Unexpected library error"
},
{
"x25519_public": "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
"rejected_with": "SmolError: rejected key agreement: Unexpected library error"
},
{
"x25519_public": "eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
"rejected_with": "SmolError: rejected key agreement: Unexpected library error"
}
],
"low_order_note": "\u00a72 and \u00a74: a received ephemeral public key that is a low-order point MUST be refused, and an all-zero X25519 output MUST be refused. libsodium reports the first as the second, since the agreement output is zero.",
"invalid_identity_keys": [
{
"identity_key": "0000000000000000000000000000000000000000000000000000000000000000",
"rejected_with": "SmolError: not a valid Ed25519 public key: Unexpected library error"
},
{
"identity_key": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"rejected_with": "SmolError: not a valid Ed25519 public key: Unexpected library error"
},
{
"identity_key": "0100000000000000000000000000000000000000000000000000000000000000",
"rejected_with": "SmolError: not a valid Ed25519 public key: Unexpected library error"
}
],
"invalid_identity_note": "Keys that do not decode as Ed25519 points, or are of small order, so \u00a72's map to X25519 has no valid result."
}
}

711
vectors.py Normal file
View file

@ -0,0 +1,711 @@
#!/usr/bin/env -S uv run --quiet --script
# /// script
# requires-python = ">=3.11"
# dependencies = ["noiseprotocol>=0.3.1", "pynacl>=1.5"]
# ///
# Copyright 2026 randogoth
# SPDX-License-Identifier: Apache-2.0
"""Test vectors for SPEC.md 1.2, generated from the reference implementation.
Every value is produced by calling into smolmail.py rather than restated here,
so the vectors cannot drift from the code they document. Each one is also
verified from the opposite side before it is emitted: signatures are checked,
envelopes are unsealed, and the rejection cases are confirmed to actually
raise. `--check` repeats the generation and compares it against the committed
files, which is what makes them a regression test rather than a snapshot.
uv run vectors.py # write vectors.json and VECTORS.md
uv run vectors.py --check # verify the committed files still reproduce
Determinism: seal() draws its ephemeral from os.urandom and its timestamp from
the clock (§5.2), so both are pinned here. Nothing else in the protocol is
randomised.
"""
from __future__ import annotations
import calendar
import contextlib
import difflib
import hashlib
import hmac
import json
import os
import re
import struct
import sys
import nacl.bindings as sodium
import smolmail as sm
# Obvious test data: two masters that no one could mistake for a real secret.
ALICE_MASTER = bytes(range(0x00, 0x20))
BOB_MASTER = bytes(range(0x20, 0x40))
SERVER_STATIC = bytes(range(0x60, 0x80))
HANDSHAKE_HASH = bytes(range(0x80, 0xA0))
WHEN = calendar.timegm((2026, 1, 1, 0, 0, 0))
FUTURE = calendar.timegm((2100, 1, 1, 0, 0, 0)) # beyond §5.3's 86400s skew
HOST = "example.org"
# §4's low-order blacklist. Every one of these must be refused as an ephemeral
# public key; the generator asserts that the reference actually refuses them.
LOW_ORDER = [
"0000000000000000000000000000000000000000000000000000000000000000",
"0100000000000000000000000000000000000000000000000000000000000000",
"e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b800",
"5f9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f1157",
"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
"edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
]
def h(raw: bytes) -> str:
return raw.hex()
@contextlib.contextmanager
def ephemeral(esk: bytes):
"""Pin seal()'s per-message ephemeral so an envelope reproduces byte for byte."""
real = os.urandom
sm.os.urandom = lambda n: esk if n == sm.KEY_LEN else real(n)
try:
yield sodium.crypto_scalarmult_base(esk)
finally:
sm.os.urandom = real
def plaintext_of(sender: sm.Identity, to: bytes, body: bytes, when: int,
epk: bytes, pad: bool) -> tuple[bytes, bytes, bytes]:
"""§5.3's payload, returned with the header and the signed preimage.
seal() builds this internally and keeps none of it, but an implementer
debugging a signature needs the exact bytes that went under the key. The
emitted envelope is asserted equal to seal()'s, which keeps this honest.
"""
header = bytes([sm.VERSION]) + sender.pk + struct.pack(">qI", when, len(body))
signed = sm.LABEL_MSG + to + epk + header + body
payload = header + body + sender.sign(signed)
if pad:
payload += bytes(-len(payload) % sm.PAD_TO)
return header, signed, payload
def encrypt(payload: bytes, to: bytes, esk: bytes) -> bytes:
"""§5.2's sealing step alone, so a vector can carry a deliberately broken
payload that still passes the AEAD."""
epk = sodium.crypto_scalarmult_base(esk)
key = sm.hkdf_sha256(sm.agree(esk, sm.ed_to_x25519_pub(to)), epk + to, sm.LABEL_SEAL)
aad = sm.MAGIC + bytes([sm.VERSION]) + to + epk
return aad + sodium.crypto_aead_chacha20poly1305_ietf_encrypt(payload, aad, bytes(12), key)
def raises(fn) -> str:
"""Run a case that must fail and return the reference's own message."""
try:
fn()
except (sm.SmolError, Exception) as exc: # noqa: BLE001 - any refusal counts
return f"{type(exc).__name__}: {exc}"
raise AssertionError("expected a rejection, got none")
# --- §2 identity ------------------------------------------------------------
def identity_vectors() -> list[dict]:
out = []
for name, master, index in [("alice", ALICE_MASTER, 0), ("alice", ALICE_MASTER, 1),
("bob", BOB_MASTER, 0)]:
seed = sm.identity_seed(master, index)
who = sm.Identity(seed)
out.append({
"name": name,
"master": h(master),
"rotation_index": index,
"seed": h(seed),
"identity_key": h(who.pk),
"x25519_private": h(who.x_priv),
"x25519_public": h(sm.ed_to_x25519_pub(who.pk)),
"base32": sm.b32(who.pk),
"fingerprint": sm.fingerprint(who.pk),
"uri": f"smol://{name}@{HOST}/{sm.b32(who.pk)}",
})
# The X25519 keypair must agree with itself, or the §2 map is wrong.
assert sodium.crypto_scalarmult_base(who.x_priv) == sm.ed_to_x25519_pub(who.pk)
return out
def accept_vectors(alice: sm.Account, bob: sm.Account) -> dict:
token = alice.token_for(bob.me.pk)
assert token == hmac.new(sm.accept_key(ALICE_MASTER), bob.me.pk, hashlib.sha256).digest()
return {
"note": "alice's accept key, and the token she issues to bob. The accept "
"key is derived without the rotation index, so it and every token "
"from it survive rotation.",
"alice_accept_key": h(sm.accept_key(ALICE_MASTER)),
"bob_accept_key": h(sm.accept_key(BOB_MASTER)),
"alice_token_for_bob": h(token),
"alice_token_for_bob_base32": sm.b32(token),
"bob_token_for_alice_0": h(bob.token_for(alice.keys[0].pk)),
"alice_token_for_bob_rotated": h(alice.token_for(
sm.Identity(sm.identity_seed(BOB_MASTER, 1)).pk)),
}
# --- §5 messages ------------------------------------------------------------
def envelope_vectors(alice: sm.Account, bob: sm.Account) -> list[dict]:
reply_to = f"smol://alice@{HOST}/{sm.b32(alice.keys[0].pk)}"
accept = sm.b32(alice.token_for(bob.me.pk))
cases = [
{"name": "empty-body", "from": ("bob", 0), "to": ("alice", 0), "pad": False,
"body": b"",
"note": "body_len 0. The payload is 109 bytes and the envelope 194."},
{"name": "unpadded", "from": ("bob", 0), "to": ("alice", 0), "pad": False,
"body": b"hello alice",
"note": "The minimal form: 69 + 45 + 11 + 64 + 16 bytes. Start here."},
{"name": "padded", "from": ("bob", 0), "to": ("alice", 0), "pad": True,
"body": b"hello alice",
"note": "The same message with §5.3 padding to a 1024-byte multiple. A "
"receiver must ignore the trailing zeros and trust body_len."},
{"name": "utf8", "from": ("bob", 0), "to": ("alice", 0), "pad": False,
"body": "Grüße aus dem Mesh — 🛰\n".encode(),
"note": "body_len counts bytes, not characters: 29 bytes, 22 characters."},
{"name": "frontmatter", "from": ("alice", 0), "to": ("bob", 0), "pad": False,
"body": sm.build_frontmatter(
[("Subject", "the thing"), ("Reply-To", reply_to), ("Accept", accept)],
"Here is a token so your mail skips my requests tier.\n").encode(),
"note": "§5.5 frontmatter carrying the reserved Reply-To (§5.7) and Accept "
"(§5.8) fields. Both are only meaningful because the signature "
"covers them."},
{"name": "superseded-key", "from": ("bob", 0), "to": ("alice", 0), "pad": False,
"body": b"sent before you rotated\n",
"note": "Sealed to alice's index-0 key while she is at index 1 (§7). She "
"opens it with the retained key, which is re-derivable from the "
"master alone."},
{"name": "rotated-key", "from": ("bob", 0), "to": ("alice", 1), "pad": False,
"body": b"sent after you rotated\n",
"note": "The same correspondents after alice's rotation. Only `to` changes; "
"her accept token does not."},
]
accounts = {"alice": alice, "bob": bob}
out = []
for n, case in enumerate(cases):
esk = bytes([0x40 + n]) * 32
sender = accounts[case["from"][0]].keys[case["from"][1]]
recipient = accounts[case["to"][0]].keys[case["to"][1]].pk
body = case["body"]
with ephemeral(esk) as epk:
env = sm.seal(sender, recipient, body, when=WHEN, pad=case["pad"])
header, signed, payload = plaintext_of(sender, recipient, body, WHEN, epk, case["pad"])
# plaintext_of/encrypt duplicate seal()'s layout; pin them to it.
assert encrypt(payload, recipient, esk) == env, case["name"]
opened = sm.unseal(accounts[case["to"][0]].keys, env)
assert opened["body"] == body and opened["sender"] == sender.pk
assert opened["time"] == WHEN and opened["id"] == sm.message_id(env)
shared = sm.agree(esk, sm.ed_to_x25519_pub(recipient))
token = accounts[case["to"][0]].token_for(sender.pk)
out.append({
"name": case["name"],
"note": case["note"],
"sender": f"{case['from'][0]}[{case['from'][1]}]",
"recipient": f"{case['to'][0]}[{case['to'][1]}]",
"body": h(body),
"body_len": len(body),
"time": WHEN,
"padded": case["pad"],
"esk": h(esk),
"epk": h(epk),
"x25519_shared": h(shared),
"seal_key": h(sm.hkdf_sha256(shared, epk + recipient, sm.LABEL_SEAL)),
"aad": h(sm.MAGIC + bytes([sm.VERSION]) + recipient + epk),
"payload_header": h(header),
"signed_preimage": h(signed),
"signature": h(payload[sm.PAYLOAD_HEADER + len(body):][:sm.SIG_LEN]),
"payload": h(payload),
"payload_len": len(payload),
"envelope": h(env),
"envelope_len": len(env),
"envelope_sha256": h(hashlib.sha256(env).digest()),
"message_id": h(sm.message_id(env)),
"accept_mac": h(sm.accept_mac(token, sm.message_id(env))),
"send_body": h(bytes([sm.TOKEN_LEN]) + sm.accept_mac(token, sm.message_id(env)) + env),
})
return out
# --- §4, §6.1 and §7 wire bytes ---------------------------------------------
def session_vectors(alice: sm.Account) -> dict:
me = alice.keys[0]
name = b"alice"
auth_sig = me.sign(sm.LABEL_AUTH + HANDSHAKE_HASH)
assert sm.verify_sig(me.pk, auth_sig, sm.LABEL_AUTH + HANDSHAKE_HASH)
token = alice.token_for(sm.Identity(sm.identity_seed(BOB_MASTER, 0)).pk)
auth_frame = (bytes([len(name)]) + name + me.pk + auth_sig
+ bytes([1]) + struct.pack(">H", 1) + token)
reg_preimage = sm.register_signed(SERVER_STATIC, "alice", me.pk)
reg_sig = me.sign(reg_preimage)
assert sm.verify_sig(me.pk, reg_sig, reg_preimage)
reg_frame = (bytes([len(name)]) + name + me.pk + reg_sig + b"\0" + b"\0")
return {
"note": "The AUTH signature covers the Noise handshake hash, so it cannot "
"be replayed to another session or server. The REGISTER signature "
"covers the server's static key for the same reason: without it, "
"any party could bind any public key to any username.",
"handshake_hash": h(HANDSHAKE_HASH),
"server_static": h(SERVER_STATIC),
"auth_signed_preimage": h(sm.LABEL_AUTH + HANDSHAKE_HASH),
"auth_signature": h(auth_sig),
"auth_frame": h(auth_frame),
"auth_frame_note": "username_len=5 || 'alice' || identity || signature || "
"sync=1 || count=1 || alice's token for bob",
"register_signed_preimage": h(reg_preimage),
"register_signature": h(reg_sig),
"register_frame": h(reg_frame),
"register_frame_note": "token_len=0 and cert_len=0: a first registration, "
"no invite token and no rotation certificate",
}
def rotation_vectors(alice: sm.Account) -> dict:
old, new = alice.keys[0], alice.keys[1]
when = struct.pack(">q", WHEN)
signed = sm.LABEL_ROTATE + b"alice" + old.pk + new.pk + when
cert = old.pk + new.pk + when + old.sign(signed) + new.sign(signed)
assert len(cert) == sm.CERT_LEN
# A chain that leads from the pinned key to the offered one is accepted; the
# same certificate checked under another username is not (§7).
assert sm.walk_chain("alice", old.pk, new.pk, [cert])
assert not sm.walk_chain("mallory", old.pk, new.pk, [cert])
assert not sm.walk_chain("alice", old.pk, new.pk, [])
forged = old.pk + new.pk + when + old.sign(signed) + bytes(sm.SIG_LEN)
assert not sm.walk_chain("alice", old.pk, new.pk, [forged])
return {
"note": "A certificate is 200 bytes and carries no username: the name is "
"covered by both signatures but not transported, so a certificate "
"cannot be replayed against another username bound to the same key.",
"username": "alice",
"old_key": h(old.pk),
"new_key": h(new.pk),
"time": WHEN,
"signed_preimage": h(signed),
"sig_old": h(old.sign(signed)),
"sig_new": h(new.sign(signed)),
"certificate": h(cert),
"resolve_body": h(new.pk + bytes([1]) + cert),
"walk_chain": [
{"case": "pinned=old, offered=new, chain=[cert], username=alice",
"accepted": True},
{"case": "same certificate checked as username=mallory",
"accepted": False,
"why": "both signatures cover the username"},
{"case": "pinned=old, offered=new, empty chain", "accepted": False,
"why": "a key change needs a chain; absent means ask the user"},
{"case": "sig_new zeroed", "accepted": False,
"why": "the new key must sign too, or the old key alone could hand "
"the username to a key nobody controls"},
],
}
# --- §3 addressing ----------------------------------------------------------
def addressing_vectors(alice: sm.Account) -> dict:
def username_ok(name: str) -> bool:
"""§3 through the real parser: a username is valid when `name@host` parses
and gives that exact username back."""
try:
return sm.Address.parse(f"{name}@{HOST}").user == name
except sm.SmolError:
return False
names = ["a", "a0", "alice", "al-ice", "a.b-c_d", "x" * 63,
"", "-alice", "alice-", ".alice", "al..ice", "a._b", "a-_b",
"Alice", "x" * 64, "al ice", "alice@", "ali/ce"]
usernames = [{"username": n, "valid": username_ok(n)} for n in names]
assert [u["valid"] for u in usernames] == [True] * 6 + [False] * 12
addresses = []
for text in [f"alice@{HOST}", f"alice@{HOST}:1961", f"alice@{HOST}:2000",
f"smol://alice@{HOST}/{sm.b32(alice.keys[0].pk)}"]:
a = sm.Address.parse(text)
addresses.append({"input": text, "user": a.user, "host": a.host,
"port": a.port, "short": a.short,
"identity": h(a.identity) if a.identity else None})
bad = {text: raises(lambda t=text: sm.Address.parse(t))
for text in [f"smol://alice@{HOST}", f"smol://alice@{HOST}/{sm.b32(bytes(31))}",
f"smol://alice@{HOST}/not-base32!", f"-alice@{HOST}"]}
return {
"note": "The short form is resolved by the server; the smol:// form carries "
"the key and needs no server to be trusted. 1961 is the default "
"port and is omitted from the short form.",
"usernames": usernames,
"addresses": addresses,
"rejected": bad,
"base32": {"identity": h(alice.keys[0].pk), "encoded": sm.b32(alice.keys[0].pk),
"fingerprint": sm.fingerprint(alice.keys[0].pk),
"fingerprint_note": "the first 20 base32 characters in groups of "
"four: 100 bits, so a collision costs ~2^100"},
}
# --- §5.5 frontmatter -------------------------------------------------------
def frontmatter_vectors() -> list[dict]:
cases = [
("no-block", "hello\n", "no leading '---\\n', so the whole body is text"),
("simple", "---\nSubject: hi\n---\nbody\n", "one field, then the text"),
("empty-block", "---\n---\n---\nliteral dashes\n",
"an empty block escapes a body that genuinely starts with '---'"),
("case-and-duplicates", "---\nSubject: first\nsubject: second\nX-Mood: calm\n---\nb",
"keys are lowercased for comparison and the first occurrence wins"),
("value-trimmed", "---\nSubject: spaced out \n---\n",
"the value is the rest of the line, trimmed"),
("colon-in-value", "---\nSubject: re: the thing\n---\n",
"only the first colon separates"),
("no-colon", "---\nSubject hi\n---\nbody\n",
"a malformed line invalidates the whole block, which becomes text"),
("bad-key", "---\nSub ject: hi\n---\nbody\n", "key charset is [A-Za-z0-9-]"),
("unterminated", "---\nSubject: hi\nbody\n", "no closing '---'"),
("too-many-keys", "---\n" + "".join(f"K{i}: v\n" for i in range(65)) + "---\nb",
"over 64 keys"),
("too-large", "---\nK: " + "v" * 4100 + "\n---\nb", "over 4 KiB"),
("injected-field", "---\nSubject: hi\nAccept: " + "a" * 8 + "\n---\nb",
"what a line break in a sender-supplied value produces: a second field, "
"signed and indistinguishable from an intended one. A client building "
"frontmatter MUST refuse a value containing a line break."),
]
out = []
for name, body, note in cases:
fields, rest = sm.parse_frontmatter(body)
out.append({"name": name, "note": note, "body": body,
"fields": fields, "text": rest,
"block_accepted": bool(fields) or rest != body})
assert out[2]["text"] == "---\nliteral dashes\n"
assert out[3]["fields"]["subject"] == "first"
assert all(not o["block_accepted"] for o in out if o["name"] in
{"no-colon", "bad-key", "unterminated", "too-many-keys", "too-large"})
return out
# --- rejections -------------------------------------------------------------
def reject_vectors(alice: sm.Account, bob: sm.Account) -> dict:
"""Inputs an implementation MUST refuse. These are the vectors that catch a
client which decrypts without verifying, or trusts `to` without checking it."""
to = alice.keys[0].pk
esk = bytes([0xA1]) * 32
with ephemeral(esk) as epk:
valid = sm.seal(bob.me, to, b"valid\n", when=WHEN, pad=False)
_, _, payload = plaintext_of(bob.me, to, b"valid\n", WHEN, epk, False)
assert sm.unseal(alice.keys, valid)["body"] == b"valid\n"
def mutate(env: bytes, at: int, raw: bytes) -> bytes:
return env[:at] + raw + env[at + len(raw):]
def repack(new_payload: bytes) -> bytes:
"""Re-seal a tampered payload so it passes the AEAD and fails deeper in."""
return encrypt(new_payload, to, esk)
bad_sig = repack(payload[:-sm.SIG_LEN] + bytes(sm.SIG_LEN))
wrong_to = repack(plaintext_of(bob.me, bob.me.pk, b"valid\n", WHEN, epk, False)[2])
long_body = repack(payload[:sm.PAYLOAD_HEADER - 4] + struct.pack(">I", 1 << 20)
+ payload[sm.PAYLOAD_HEADER:])
_, _, future = plaintext_of(bob.me, to, b"later\n", FUTURE, epk, False)
envelopes = [
("bad-magic", mutate(valid, 0, b"XMOL"), "magic is not 'SMOL' (§5.1)"),
("bad-envelope-version", mutate(valid, 4, b"\x02"),
"unknown envelope version; the sealed-box construction may differ"),
("truncated", valid[:sm.ENVELOPE_HEADER],
"shorter than the 69-byte header plus a 16-byte tag"),
("tag-flipped", valid[:-1] + bytes([valid[-1] ^ 0x01]),
"the Poly1305 tag no longer authenticates"),
("aad-tampered", mutate(valid, 40, bytes([valid[40] ^ 0x01])),
"epk is covered by the AAD, so altering it breaks the tag rather than "
"silently re-addressing the message"),
("not-for-us", repack(payload)[:5] + bob.me.pk + valid[37:],
"`to` names a key we do not hold; a client MUST check it against its own"),
("bad-signature", bad_sig,
"decrypts cleanly and the signature is zeroed. An implementation that "
"skips §5.3 verification will accept forged mail here."),
("signature-bound-to-other-recipient", wrong_to,
"a payload signed for bob, resealed to alice. The signature covers `to`, "
"so re-addressing a recorded message must not verify."),
("body-len-overflows", long_body, "body_len exceeds the payload (§5.3)"),
("payload-version", repack(b"\x02" + payload[1:]), "unknown payload version"),
("dated-in-the-future", repack(future),
f"time is {FUTURE}, more than 86400s ahead of any clock before 2100 (§5.3)"),
("low-order-epk", mutate(valid, 37, bytes.fromhex(LOW_ORDER[2])),
"a low-order ephemeral key, which §2 requires be refused"),
]
out = []
for name, env, why in envelopes:
out.append({"name": name, "why": why, "envelope": h(env),
"envelope_len": len(env),
"rejected_with": raises(lambda e=env: sm.unseal(alice.keys, e))})
low = [{"x25519_public": p,
"rejected_with": raises(lambda p=p: sm.agree(alice.keys[0].x_priv,
bytes.fromhex(p)))}
for p in LOW_ORDER]
keys = [{"identity_key": k,
"rejected_with": raises(lambda k=k: sm.ed_to_x25519_pub(bytes.fromhex(k)))}
for k in ["0000000000000000000000000000000000000000000000000000000000000000",
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"0100000000000000000000000000000000000000000000000000000000000000"]]
return {
"note": "Inputs a conformant implementation refuses. The envelope cases are "
"mutations of one valid message, listed with the reference's own "
"error, so a client can be tested against its own failure path.",
"valid_baseline": h(valid),
"envelopes": out,
"low_order_x25519": low,
"low_order_note": "§2 and §4: a received ephemeral public key that is a "
"low-order point MUST be refused, and an all-zero X25519 "
"output MUST be refused. libsodium reports the first as "
"the second, since the agreement output is zero.",
"invalid_identity_keys": keys,
"invalid_identity_note": "Keys that do not decode as Ed25519 points, or are "
"of small order, so §2's map to X25519 has no "
"valid result.",
}
# --- assembly ---------------------------------------------------------------
def build() -> dict:
alice = sm.Account(ALICE_MASTER, 1) # rotated once, so §7 has both keys
bob = sm.Account(BOB_MASTER, 0)
return {
"spec": "smolmail/1.2",
"generated_by": "vectors.py, from smolmail.py",
"note": "All values are hex unless the field name says otherwise. `alice[n]` "
"and `bob[n]` name a master and a rotation index from `identity`. "
"Masters are test data: never use them.",
"labels": {name: getattr(sm, name).decode() for name in
["LABEL_IDENTITY", "LABEL_ACCEPT", "LABEL_SEAL", "LABEL_MSG",
"LABEL_ID", "LABEL_MAC", "LABEL_AUTH", "LABEL_ROTATE",
"LABEL_REGISTER", "PROLOGUE"]},
"constants": {"default_port": sm.DEFAULT_PORT, "magic": sm.MAGIC.decode(),
"version": sm.VERSION, "envelope_header": sm.ENVELOPE_HEADER,
"payload_header": sm.PAYLOAD_HEADER, "pad_to": sm.PAD_TO,
"max_chain": sm.MAX_CHAIN, "max_skew": sm.MAX_SKEW,
"noise_protocol": sm.NOISE_PROTOCOL.decode()},
"identity": identity_vectors(),
"accept": accept_vectors(alice, bob),
"addressing": addressing_vectors(alice),
"envelopes": envelope_vectors(alice, bob),
"session": session_vectors(alice),
"rotation": rotation_vectors(alice),
"frontmatter": frontmatter_vectors(),
"rejects": reject_vectors(alice, bob),
}
def wrap(value: str, width: int = 76) -> str:
return "\n".join(value[i:i + width] for i in range(0, len(value), width))
def field(key: str, value, limit: int = 160) -> str:
"""Long hex goes in a fenced block; everything short stays on one line."""
if isinstance(value, str) and len(value) > limit:
return f"\n`{key}`\n\n```\n{wrap(value)}\n```\n"
if isinstance(value, bool):
value = "true" if value else "false"
return f"- `{key}`: `{value}`\n" if value != "" else f"- `{key}`: (empty)\n"
def markdown(v: dict) -> str:
L = v["labels"]
md = [
"<!-- SPDX-License-Identifier: CC-BY-SA-4.0 -->",
"",
f"# smolmail Test Vectors, version {v['spec'].split('/')[1]}",
"",
"Known-answer tests for [SPEC.md](SPEC.md), generated from the reference "
"implementation by [vectors.py](vectors.py). [vectors.json](vectors.json) "
"carries the same values in a form a test harness can load; this file is "
"for reading alongside the specification.",
"",
"Work through them in order. Derivation first (§2), because every later "
"vector depends on it, then an envelope you can seal and unseal (§5), then "
"the wire frames (§4, §6.1, §7). Finish with the rejections: they are the "
"ones that catch an implementation which decrypts without verifying.",
"",
"Hex throughout, unless a field name says otherwise. The two masters are "
"test data and the private keys here are public. Never use them for "
"anything.",
"",
"## Labels (§11)",
"",
"Every hash, signature and MAC is domain-separated by one of these, "
"prefixed to the input as raw bytes with no separator or length:",
"",
]
md += [f"- `{k}` = `{val}`" for k, val in L.items()]
md += ["", "## §2 Identity", "",
"`seed_n = HKDF-SHA256(master, salt = \"\", info = "
f"\"{L['LABEL_IDENTITY']}\" || n)`, with `n` a big-endian u32. The "
"Ed25519 public key of that seed *is* the identity. The X25519 keypair "
"comes from the same key via libsodium's conversions, and the accept key "
"is derived without the index so it survives rotation.", ""]
for who in v["identity"]:
md.append(f"### {who['name']}, rotation index {who['rotation_index']}")
md.append("")
for key in ["master", "rotation_index", "seed", "identity_key",
"x25519_private", "x25519_public", "base32", "fingerprint", "uri"]:
md.append(field(key, who[key]).rstrip("\n"))
md.append("")
md += ["### Accept keys and tokens (§5.8)", "",
v["accept"]["note"], ""]
md += [field(k, val).rstrip("\n") for k, val in v["accept"].items() if k != "note"]
md += ["",
"`alice_token_for_bob_rotated` is the token alice would issue to bob's "
"index-1 key. It differs from the one bob already holds, which is why "
"§5.8 says a token is derived from the correspondent's identity *as it "
"stood when they were accepted*: bob's rotation does not invalidate the "
"token he was given.", ""]
a = v["addressing"]
md += ["## §3 Addressing", "", a["note"], "", "### Usernames", "",
"| username | valid |", "| --- | --- |"]
md += [f"| `{u['username'] or '(empty)'}` | {'yes' if u['valid'] else 'no'} |"
for u in a["usernames"]]
md += ["", "### Addresses", "",
"| input | user | host | port | identity |", "| --- | --- | --- | --- | --- |"]
for ad in a["addresses"]:
ident = f"`{ad['identity'][:16]}…`" if ad["identity"] else "resolved"
md.append(f"| `{ad['input']}` | `{ad['user']}` | `{ad['host']}` | "
f"{ad['port']} | {ident} |")
md += ["", "Refused:", ""]
md += [f"- `{text}` — {why}" for text, why in a["rejected"].items()]
md += ["", "### Fingerprint", ""]
md += [field(k, val).rstrip("\n") for k, val in a["base32"].items()]
md += ["", "## §5 Messages", "",
"Each vector pins the ephemeral `esk` that §5.2 would draw at random and "
"the `time` that §5.3 would take from the clock. Nothing else in the "
"protocol is randomised, so with those two fixed an envelope reproduces "
"byte for byte.", "",
"Build one in this order: agree on `x25519_shared`, derive `seal_key`, "
"assemble `payload_header`, sign `signed_preimage`, concatenate the "
"payload, then encrypt it under `aad` with an all-zero nonce. If your "
"`seal_key` matches and your envelope does not, the fault is in the "
"payload layout; if `x25519_shared` already differs, it is in §2's map.",
"",
"`accept_mac` is what a sender holding the recipient's token attaches to "
"`SEND` (§5.8); `message_id` is what the server returns. The full payload "
"plaintext and the whole `SEND` request body are in vectors.json as "
"`payload` and `send_body`.", ""]
for e in v["envelopes"]:
md += [f"### {e['name']}", "", e["note"], ""]
for key in ["sender", "recipient", "body", "body_len", "time", "padded",
"esk", "epk", "x25519_shared", "seal_key", "aad",
"payload_header", "signed_preimage", "signature",
"payload_len", "envelope_len", "envelope", "envelope_sha256",
"message_id", "accept_mac"]:
md.append(field(key, e[key]).rstrip("\n"))
md.append("")
s = v["session"]
md += ["## §4 and §6.1 Session frames", "", s["note"], ""]
for key in ["handshake_hash", "server_static", "auth_signed_preimage",
"auth_signature", "auth_frame"]:
md.append(field(key, s[key]).rstrip("\n"))
md += ["", s["auth_frame_note"], ""]
for key in ["register_signed_preimage", "register_signature", "register_frame"]:
md.append(field(key, s[key]).rstrip("\n"))
md += ["", s["register_frame_note"], ""]
r = v["rotation"]
md += ["## §7 Key rotation", "", r["note"], ""]
for key in ["username", "old_key", "new_key", "time", "signed_preimage",
"sig_old", "sig_new", "certificate", "resolve_body"]:
md.append(field(key, r[key]).rstrip("\n"))
md += ["", "Chain walking:", ""]
md += [f"- {c['case']} → **{'accept' if c['accepted'] else 'refuse'}**"
+ (f" ({c['why']})" if c.get("why") else "") for c in r["walk_chain"]]
md += ["", "## §5.5 Frontmatter", "",
"A flat `Key: value` block, not YAML. A malformed line invalidates the "
"whole block, which is then shown as ordinary body text: frontmatter "
"fails closed toward display, never toward silent discard.", "",
"| case | input | fields | text | note |", "| --- | --- | --- | --- | --- |"]
def clip(text: str, keep: int = 48) -> str:
return repr(text) if len(text) <= keep else repr(text[:keep]) + "…"
for f in v["frontmatter"]:
md.append(f"| `{f['name']}` | `{clip(f['body'])}` | `{f['fields']}` | "
f"`{clip(f['text'])}` | {f['note']} |")
md += ["", "Long inputs are clipped here; vectors.json has them whole."]
rj = v["rejects"]
md += ["", "## Rejections", "", rj["note"], "",
"Each envelope is a mutation of this valid one:", "",
field("valid_baseline", rj["valid_baseline"]).rstrip("\n"), ""]
for case in rj["envelopes"]:
md += [f"### {case['name']}", "", case["why"], "",
field("envelope", case["envelope"]).rstrip("\n"),
field("rejected_with", case["rejected_with"]).rstrip("\n"), ""]
md += ["### Low-order X25519 keys", "", rj["low_order_note"], ""]
md += [f"- `{p['x25519_public']}`" for p in rj["low_order_x25519"]]
md += ["", "### Invalid identity keys", "", rj["invalid_identity_note"], ""]
md += [f"- `{k['identity_key']}` — {k['rejected_with']}"
for k in rj["invalid_identity_keys"]]
md += ["", "## License", "",
"These vectors are licensed under "
"[CC BY-SA 4.0](LICENSES/CC-BY-SA-4.0.txt), with the specification.", ""]
return re.sub(r"\n{3,}", "\n\n", "\n".join(md))
def main(argv: list[str]) -> int:
check = "--check" in argv
built = build()
rendered = markdown(built)
here = os.path.dirname(os.path.abspath(__file__))
files = {"vectors.json": json.dumps(built, indent=2) + "\n",
"VECTORS.md": rendered}
if not check:
for name, text in files.items():
with open(os.path.join(here, name), "w", encoding="utf-8") as fh:
fh.write(text)
print(f"wrote {name} ({len(text)} bytes)")
counts = {k: len(built[k]) for k in ["identity", "envelopes", "frontmatter"]}
print(f"{counts['identity']} identities, {counts['envelopes']} envelopes, "
f"{len(built['rejects']['envelopes'])} rejections, "
f"{counts['frontmatter']} frontmatter cases: all verified")
return 0
failed = False
for name, text in files.items():
path = os.path.join(here, name)
try:
with open(path, encoding="utf-8") as fh:
have = fh.read()
except FileNotFoundError:
print(f"{name}: missing")
failed = True
continue
if have == text:
print(f"{name}: ok")
continue
failed = True
print(f"{name}: differs from what smolmail.py now produces")
for line in list(difflib.unified_diff(
have.splitlines(), text.splitlines(),
fromfile=f"{name} (committed)", tofile=f"{name} (regenerated)",
lineterm=""))[:40]:
print(f" {line}")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))