# Signatures

> What BatteryPass signs in each passport version, how the hash and the Ed25519 signature are computed, and how to check them with the published keys.

Every published passport version is signed, so anyone can check that the data comes from your BatteryPass
installation and was not changed afterwards. The check needs no account and no call to BatteryPass: the public
JSON and the published keys are enough.

## What is signed

At publication BatteryPass signs two documents of each version with the active key, in the same transaction
that stores them:

| Document        | Contains                                                           | Hash and signature                | Where to read it                                                                                                                                                                                                                                                                      |
| --------------- | ------------------------------------------------------------------ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Public document | The `public` tier projection, with labels                          | `public_hash`, `public_signature` | `document` and `verification` of the public JSON ([`getPublicPassport`](https://passport.kabin.sh/docs/en/api-reference/public/get-public-passport)); `public_document` of [`getPassportVersion`](https://passport.kabin.sh/docs/en/api-reference/passports/get-passport-version) |
| Snapshot        | Every field in every tier, with provenance and document references | `canonical_hash`, `signature`     | `snapshot` of [`getPassportVersion`](https://passport.kabin.sh/docs/en/api-reference/passports/get-passport-version), for your organisation                                                                                                                                         |

The public document is what a scanner sees, so anyone can verify it. The snapshot proves the full record to you
and to authorities you show it to. In the public JSON only `document` is signed; its siblings `passport_id`,
`status`, `valid_until`, `verification` and `links` change over the passport's life and are not covered.

```json title="verification"
{
  "algorithm": "Ed25519",
  "canonicalization": "RFC8785",
  "hash": "sha256:22471733ece6d811d244144731fc505180c5e7760a7a016c1322338bce0f56e5",
  "signature": "w0nnz3kjhrEGb1DtMoGp9qlH9g8q0h9YDJRb2jfabq7AOzPMUpnWXoxmIlptgJPTDVy3LWxLQkOpQYuGYXgJ4w",
  "key_id": "2027-10-k2",
  "signed_at": "2026-10-24T09:15:02Z",
  "keys_url": "https://batterypass.example.com/.well-known/batterypass/keys.json"
}
```

## How a signature is made and checked

1. **Canonicalise**

   Serialise the signed object (`document`, or `snapshot`) with the JSON Canonicalization Scheme, JCS
   (RFC 8785). JCS sorts object members, drops whitespace and writes numbers and strings in one defined form,
   so the same data always gives the same bytes. Canonicalise the parsed object, not the response bytes.

2. **Hash**

   Compute SHA-256 over the canonical bytes and write it as `sha256:` followed by the lowercase hex digest.
   It must equal `verification.hash` (or `public_hash`, `canonical_hash`).

3. **Find the key**

   Fetch the JWK set from `/.well-known/batterypass/keys.json` of the BatteryPass address you trust and take the
   key whose `kid` equals `verification.key_id`. `keys_url` is not signed: if it points to another address, treat
   the check as failed.

4. **Verify**

   Decode the key's `x` and the signature from base64url without padding (32 and 64 bytes) and check the
   Ed25519 signature over the canonical bytes, not over the hash.

The [signature verification guide](https://passport.kabin.sh/docs/en/guides/verify-signature) does this step by step with complete code.

## The published keys

The public keys are a JWK set at `/.well-known/batterypass/keys.json` on your BatteryPass address, outside
`/api/v1`. It needs no key, may be read from any origin (CORS) and is cacheable for an hour.

```json title="keys.json"
{
  "keys": [
    { "kty": "OKP", "crv": "Ed25519", "x": "WuoaQRoQuh2kPs5yYB6HxNz3Y58ndE8fRHnzG10NxoU", "kid": "2027-10-k2", "use": "sig", "alg": "EdDSA" },
    { "kty": "OKP", "crv": "Ed25519", "x": "JEpD6r2NnmNrL4_7UWS9wIWLFyjCfaSClypHm110Vl4", "kid": "2026-10-k1", "use": "sig", "alg": "EdDSA" }
  ]
}
```

The active key is listed first. The operator of your installation rotates keys: a new key signs new versions,
and the old public keys stay in the set so earlier signatures keep verifying. A key that signed a published
version is never removed. Cache keys by `kid` and fetch the set again when you meet an unknown `kid`.

## Verifying on the server or offline

|                | [`verifyPassport`](https://passport.kabin.sh/docs/en/api-reference/public/verify-passport)          | Offline verification                                    |
| -------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| What it checks | Both signatures and both hashes of a version against the stored documents                             | The public document you hold, against the published key |
| Result         | `valid`, `public_document_valid`, `snapshot_valid`, `key_id`, `version_no`, `signed_at`, `checked_at` | Your own yes or no                                      |
| Trust          | You trust the server's answer                                                                         | You trust only the key, which you can pin               |
| Needs          | The passport UUID; `version_no` optional (default: current version)                                   | The public JSON and the JWK set                         |

`verifyPassport` is a convenience for a quick check. For an independent proof, for example in an archive or a
customer's system, verify offline.

> [!NOTE]
> A valid signature shows that this BatteryPass installation published exactly this data in this version. It
> does not say that the data is correct or complete, and it is not a qualified electronic seal under eIDAS.
