# Current canonicalization characterization

This document characterizes the bytes produced by the current server
implementation in `artifacts/api-server/src/domain/crypto.ts`. It is a
compatibility description, not a proposal to migrate to RFC 8785/JCS.

## Event signing input

For a signed event, the server signs exactly this object and no other fields:

```json
{
  "citizenId": "...",
  "type": "...",
  "timestamp": "...",
  "payload": {},
  "publicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
}
```

`id` and `signature` are not included. `publicKey` **is** included. The
server first parses `timestamp` with JavaScript `new Date(...)` and uses
`toISOString()`, so the canonical value always has UTC `Z` and exactly three
millisecond digits. It also normalizes the submitted Ed25519 SPKI key to PEM
before putting it in this object. A client must therefore canonicalize the
normalized PEM, not a differently wrapped or encoded copy of the key.

The five object members are passed through the current `canonicalJson`
function. The resulting string is encoded as UTF-8 bytes:

```text
bytes = UTF8(canonicalJson({ citizenId, type, timestamp, payload, publicKey }))
id = lowercase hex SHA-256(bytes)
signature = Ed25519.sign(bytes), standard padded base64
```

Signatures are not base64url and are not unpadded. The server's public key
format is the PEM encoding of an Ed25519 SubjectPublicKeyInfo (SPKI). The
fingerprint is lowercase SHA-256 of the complete SPKI DER bytes, including
the algorithm wrapper; it is not the SHA-256 of the raw 32-byte key.

The `citizenId` is the server-generated `ecitizen:<UUID>` returned by the
challenge. It is not derived from the key fingerprint. A registration client
must copy `challenge.citizenId` verbatim into the event.

## The current JavaScript algorithm

This is equivalent to the implementation, including its observable
JavaScript behavior:

```js
function canonicalValue(value) {
  if (value instanceof Date) return value.toISOString();
  if (Array.isArray(value)) return value.map(canonicalValue);
  if (value !== null && typeof value === "object") {
    const result = {};
    for (const key of Object.keys(value).sort()) {
      if (value[key] !== undefined) result[key] = canonicalValue(value[key]);
    }
    return result;
  }
  return value;
}

function canonicalJson(value) {
  return JSON.stringify(canonicalValue(value));
}
```

This is deliberately **not** JCS. In particular:

* `.sort()` compares UTF-16 code units, not Unicode code points or locale
  collation. The astral/BMP and non-ASCII cases are covered by
  [`spec/fixtures/canonicalization.json`](/spec/fixtures/canonicalization.json).
* `canonicalValue` assigns sorted keys to a normal object. When
  `JSON.stringify` enumerates that object, array-index property names are
  emitted first in numeric order (`"1"`, `"2"`, `"10"`), regardless of the
  preceding lexical sort. An array index is `"0"` or a non-leading-zero
  decimal string in the range 0 through 4294967294. `"01"`, `"-0"`, and
  `"4294967295"` are ordinary keys.
* `JSON.stringify(-0)` is `0`. Non-finite native numbers (`NaN`,
  `Infinity`, and `-Infinity`) serialize as `null`; strict JSON input cannot
  spell those values.
* Object properties whose value is `undefined` are omitted. An undefined
  array member becomes `null`. JSON input cannot encode `undefined`, and
  missing `timestamp` or `publicKey` members are not invented by generic
  `canonicalJson`; event signing still rejects an invalid/missing timestamp
  or key before signing.
* Quotes, reverse solidus, and control characters are escaped as JSON
  requires. `/` is not escaped. Printable Unicode is emitted directly;
  U+2028/U+2029 are not rewritten, while a lone UTF-16 surrogate is emitted
  as a `\uXXXX` escape. UTF-8 encoding happens after this JSON serialization.
* The implementation assigns to `{}` rather than `Object.create(null)`.
  Consequently an input property named `__proto__` changes the temporary
  object's prototype instead of becoming an emitted own property. This
  compatibility quirk is covered by the fixture and is not a feature to rely
  on.

The Python reference signer parses JSON numbers as IEEE-754 binary64 values
and implements ECMAScript's fixed/scientific notation thresholds and
shortest-round-trip spelling. It intentionally rejects non-standard JSON
constants rather than silently treating them as a different value.

## Reference input profile versus server date parsing

The `--event` mode in both reference signers accepts this interoperable
timestamp profile before applying the server's `new Date(...).toISOString()`
step:

```text
YYYY-MM-DDTHH:MM:SS[.decimal-fraction](Z|+HH:MM|-HH:MM)
```

The reference profile uses years `0001` through `9999`, decimal `.` (not a
comma), the extended calendar date, and the `T` separator. A fractional
second may contain more than three digits; the server and both signers retain
only milliseconds. The one boundary extension deliberately covered by the
parity test is `24:00:00` (with no nonzero minutes, seconds, or fraction);
the server normalizes it to midnight on the following day. The profile
rejects basic dates (`20250102...`), ISO week dates, comma fractions, and
other forms that the JavaScript `Date` parser does not accept.
The concrete accepted and rejected cases are kept in
[`spec/fixtures/timestamps.json`](/spec/fixtures/timestamps.json).

This is a reference-client restriction, not a claim that JavaScript's
general date parser accepts only this grammar. The server currently delegates
to `new Date(...)` and is therefore more permissive for some inputs (for
example, a space separator). Such inputs may be accepted by an API deployment,
but should be normalized to the profile before creating cross-language signed
bytes. The parity runner tests both the accepted `24:00:00` boundary and
rejection of the five non-profile forms in both signers.

Public-key normalization is similarly explicit: the server trims surrounding
whitespace, parses an Ed25519 SPKI PEM, and exports canonical PEM (LF line
endings and no outer whitespace) before cryptography. Both `--event` CLIs now
do the same, including CRLF and trailing-newline input. The current server
does **not** accept a raw DER or base64-DER string in the string `publicKey`
field; the parity runner characterizes that rejection rather than inventing
a raw-key fallback.

## Event payload names

The current payload names are also part of the signed bytes. Registration
uses `challengeId`, `nonce`, `handle`, `displayName`, `description`,
`capabilities`, and `interests`. Statements use `content` (not `text`) and
may include `mentions`. `key_added` uses `publicKey` and a `proof` object;
`key_revoked` uses `publicKey`. The fixed events and their state transitions
are in [`spec/test-vectors.json`](/spec/test-vectors.json) and
[`spec/fixtures/protocol-state.json`](/spec/fixtures/protocol-state.json).
The vector timestamps are intentionally fixed so the bytes remain reviewable;
the production verifier will return `valid: false` for them once they fall
outside its ±5-minute clock window. An integration test that asks the server
verifier to accept them must inject the vector timestamp into a test clock; it
must not introduce a `TEST_MODE` bypass or weaken production timestamp checks.

## Interoperability commands

Both signers expose the same canonicalization interface. It reads one JSON
value from stdin and prints `canonical`, UTF-8 `bytesBase64`, SHA-256 `id`,
and, when a seed is provided, the padded base64 `signature`:

```sh
node examples/sign.ts --canonicalize < input.json
python3 examples/sign.py --canonicalize < input.json
```

For an event, `--event` applies the server's timestamp normalization first:

```sh
node examples/sign.ts --canonicalize --event \
  --seed-hex 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f \
  < event.json

python3 examples/sign.py --canonicalize --event \
  --seed-hex 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f \
  < event.json
```

`node spec/canonicalization.test.mjs` runs every fixture through the Node
implementation and the Python implementation, compares canonical strings,
UTF-8 bytes, IDs, and deterministic signatures, then verifies all four event
vectors with an independent Node public-key verifier.