# Hashing Profile v1

Portable, versioned description of *how* a content hash inside a Decision
Receipt was computed, so that a verifier can reproduce the same commitment
years later from the original content — without needing any specific piece
of Signatrust code.

Status: **DOCUMENTED**, with the reference implementation in
`src/hashingProfile.ts`. Deployment of the profile field is additive and
backward compatible; receipts without a profile remain verifiable
cryptographically, but the verifier will report that content
re-verification of `input_hash` / `output_hash` is **not possible**
without an out-of-band description of how those hashes were computed.

## Problem statement

A receipt can carry `decision.input_hash: sha256:aa…` and
`decision.output_hash: sha256:bb…`. Given the raw content five years later,
an evaluator cannot recompute those hashes unless they know:

- which UTF-8 normalisation form was applied (if any),
- whether trailing whitespace / BOM / CRLF was stripped,
- whether JSON was canonicalised, and if so with what rules,
- whether a salt was used, and where the salt came from,
- whether the hash was computed over raw bytes or a transformed string.

Recording the hash string alone commits nothing except "somebody produced
this 32-byte value". A hashing profile fixes the missing procedural detail.

## Profile schema

A profile is a JSON object appearing as an optional field on
`DecisionInfo`:

```jsonc
{
  "hash_algorithm": "sha256",
  "content_type": "text/plain; charset=utf-8",
  "encoding": "utf-8",
  "normalization": "NFC",
  "canonicalization_profile": "none",
  "salt_mode": "none",
  "salt_reference": null,
  "hash_profile_version": "signatrust-hash-profile-v1"
}
```

### Field semantics

| Field | Values | Meaning |
|-------|--------|---------|
| `hash_algorithm` | `"sha256"` | The digest algorithm. Only SHA-256 is defined for v1. |
| `content_type` | An IANA media type with `charset` for text types. | The interpretation the issuer applied to the byte stream before hashing. |
| `encoding` | `"utf-8"` \| `"binary"` \| `"base64"` | Encoding of the input to the normaliser. |
| `normalization` | `"none"` \| `"NFC"` \| `"NFKC"` \| `"NFD"` \| `"NFKD"` \| `"strip-cr"` \| `"NFC+strip-cr"` | Unicode / line-ending normalisation applied before canonicalisation. |
| `canonicalization_profile` | `"none"` \| `"signatrust-deterministic-json-v1"` \| `"rfc-8785-jcs"` | Structural canonicalisation. `signatrust-deterministic-json-v1` is defined in `src/canonical.ts`; `rfc-8785-jcs` is claimed only when the implementation passes JCS conformance vectors, which the reference implementation **does not yet do**, so this value is reserved. |
| `salt_mode` | `"none"` \| `"static"` \| `"per_receipt"` \| `"hkdf"` | Whether a salt was prepended. |
| `salt_reference` | `null` \| `"sha256:<hex>"` \| structured object | Commitment to the salt, so the verifier can determine whether a proposed salt is the same one used at seal time — without revealing the salt itself, if it must remain secret. |
| `hash_profile_version` | `"signatrust-hash-profile-v1"` | Identifier of *this* profile schema. |

### The `signatrust-deterministic-json-v1` canonicalisation profile

Definition (normative for this codebase):

1. Values are one of: `null`, boolean, finite number, string, array,
   object. `undefined` values are dropped from objects; NaN / ±Infinity
   are rejected.
2. Object keys are sorted lexicographically by Unicode code point.
3. Arrays preserve their original order.
4. The result is serialised with `JSON.stringify` in an ES2020+ runtime
   (no trailing whitespace, no fancy indenting, escape only what
   `JSON.stringify` escapes).
5. Numbers use the standard `JSON.stringify` representation (so
   `1.0 → "1"`, `1.5 → "1.5"`, `1e20 → "100000000000000000000"`).
6. Strings are treated as opaque UTF-16 code units; no NFC is applied
   inside canonicalisation. Unicode normalisation is a **pre-canonical**
   step, controlled by `normalization`.

This profile is deliberately **not RFC 8785 JCS**. Reason: v8785 mandates
specific number formatting (ECMAScript Number.prototype.toString), which
the reference implementation does not test conformance against yet. To
claim RFC 8785 in a receipt, the implementation must first pass the JCS
conformance vectors; until then, receipts should carry
`"signatrust-deterministic-json-v1"`.

### Salt semantics

A salt is used when the plaintext being hashed has low entropy (for
example, a boolean claim or a categorical enum), and a raw SHA-256 would
allow trivial guessing by the verifier. Modes:

- `"none"`: no salt.
- `"static"`: a single salt is reused; `salt_reference` MUST commit to it
  (typically `sha256:<hex>` of the salt) so the verifier can confirm the
  salt they hold is the one the issuer used.
- `"per_receipt"`: the salt is unique per receipt; `salt_reference` MUST
  be a structured object explaining how the salt was derived (for example
  `{ "source": "receipt.id", "kdf": "hkdf-sha256", "info": "input" }`).
- `"hkdf"`: same as `per_receipt` but with a keyed input material; the
  salt itself may be held by the customer only.

The salt itself is **never** transmitted in the receipt. Only the
commitment / derivation description is signed.

## Where the profile lives on a receipt

Additive optional fields on `DecisionInfo`:

```ts
interface DecisionInfo {
  // …existing fields…
  input_profile?: HashingProfile;
  output_profile?: HashingProfile;
}
```

Legacy receipts have neither field. The verifier does not treat this as an
error; it treats it as a **known limitation**:

> Content re-verification of `input_hash` / `output_hash` is not possible:
> no `input_profile` / `output_profile` recorded. Cryptographic integrity
> of the receipt itself is unaffected.

## Cross-language reproducibility guarantees

The reference implementation lives in:

- JS/TS: `src/hashingProfile.ts` and `src/canonical.ts`.
- Python: the SDK's `signatrust.hashing_profile` module MUST mirror the
  same profile ids and produce the same digests for identical inputs.

Reproducibility guarantees (subject to the mandatory tests below):

1. Same content + same profile ⇒ same digest, byte-for-byte, on both JS
   and Python reference implementations.
2. For `canonicalization_profile = signatrust-deterministic-json-v1`:
   reordering keys in the input JSON does not change the digest.
3. For `content_type = text/plain; charset=utf-8` with
   `normalization = NFC`: canonically equivalent Unicode strings hash to
   the same digest.
4. Changing any single meaningful bit of content changes the digest.

## Mandatory tests

See `scripts/regression-matrix.ts` §hashing:

- `same_content_same_profile_same_hash_js_python`
- `canonical_json_reorder_invariance`
- `unicode_nfc_edge`
- `unicode_nfkc_edge`
- `crlf_vs_lf_when_strip_cr`
- `low_entropy_salted_static`
- `low_entropy_salted_per_receipt`
- `legacy_receipt_without_profile_still_verifies_cryptographically`
- `content_reverification_impossible_reported_when_profile_missing`
- `unknown_profile_id_rejected_by_verifier`

## What this does NOT claim

- It does not prove the input/output content was correct.
- It does not prove the content is complete relative to the real-world
  decision context.
- It does not authenticate the source of the raw content — only that
  whoever computed the hash followed the declared profile.
- It does not make legacy receipts "compliant"; it makes future receipts
  independently re-verifiable at the content layer.
