diff --git a/docs/repos/cryptify.md b/docs/repos/cryptify.md index 2f6d0a9..2a0173d 100644 --- a/docs/repos/cryptify.md +++ b/docs/repos/cryptify.md @@ -33,10 +33,11 @@ Configuration parameters: | `staging_mode` | When `true`, `send_email` skips SMTP entirely and logs the intended email metadata at info level. The upload finalize still returns `Ok`. Defaults to `false`. Intended for staging deploys where real email delivery is undesirable | `false` | | `usage_db` | Path to the SQLite database used for upload usage accounting | `/app/data/usage.db` | | `metrics_scan_interval_secs` | Interval in seconds for the background task that samples `data_dir` size and file count for the storage gauges exposed at `GET /metrics`. Defaults to `60`. | `60` | +| `email_attribute` | Attribute type carrying the sender's email in the signing identity. Finalize requires this attribute to be present on the uploader's identity. Production keeps the default. Test environments override it with a test-scheme type. Must match the PKG's `PKG_EMAIL_ATTRIBUTE` and the SDK's `emailAttributes`. Defaults to `pbdf.sidn-pbdf.email.email`. | `irma-demo.sidn-pbdf.email.email` | The `chunk_size` setting caps the size of each `PUT /fileupload/{uuid}` body. Clients (such as `@e4a/pg-js` and the PostGuard website) use the same value for their upload chunks, so increasing it server-side without updating the client default will not produce larger chunks on its own. -[Source: src/config.rs#L3-L38](https://github.com/encryption4all/cryptify/blob/2af3ba0736ba138343295669411334af6f6de37a/src/config.rs#L3-L38) +[Source: src/config.rs#L3-L22](https://github.com/encryption4all/cryptify/blob/e4a618249dee43d7fe9086844dc122186b3ea07f/src/config.rs#L3-L22) ### Staging mode @@ -80,7 +81,7 @@ When a request would push the sender over the per-upload or the rolling-window l `limit` is either `"per_upload"` or `"rolling_window"`. `resets_at` is an RFC 3339 timestamp for when the oldest counted upload expires from the rolling window. It is `null` for `per_upload` rejections, since the per-upload limit does not reset. -`GET /usage` returns the current state for the authenticated sender, including `used_bytes`, `limit_bytes`, `per_upload_limit_bytes`, `window_days`, and `resets_at`. When the request includes a validated `Authorization: Bearer PG-…`, the response describes the per-tenant bucket (`api-key:`); otherwise it describes the per-email bucket. +`GET /usage` returns the current state for the caller, including `used_bytes`, `limit_bytes`, `per_upload_limit_bytes`, `window_days`, and `resets_at`. It requires a validated `Authorization: Bearer PG-…` API key and describes that key's per-tenant bucket (`api-key:`). A missing or invalid key returns `401`; if the key cannot be confirmed because PKG is unreachable, the endpoint returns `503`. The `email` query parameter is optional and only echoed back in the response, it no longer selects which bucket is reported. [Source: src/store.rs#L11-L15](https://github.com/encryption4all/cryptify/blob/58883a86b369af08d92db93aa1025f9eba3c73eb/src/store.rs#L11-L15) @@ -114,8 +115,24 @@ Cryptify exposes a file upload/download API. An OpenAPI 3.0 specification is ava - `POST /fileupload/finalize/{uuid}`: Finalize the upload (sends the recipient notification email if `notifyRecipients` was `true` on init). - `GET /fileupload/{uuid}/status`: Read rolling-token state to resume an in-flight upload across a page refresh or tab crash. Authenticated via `X-Recovery-Token`. - `GET /filedownload/{uuid}`: Download a file. Supports resumable downloads via the HTTP `Range` header (see [Range support on `/filedownload`](#range-support-on-filedownload) below). +- `GET /email-template`: Return the email template pg-pkg has linked to the caller's API key. Authenticated with the same `Authorization: Bearer PG-…` key as the upload endpoints (see [Email template retrieval](#email-template-retrieval) below). - `GET /metrics`: Prometheus text-format metrics for monitoring (see [Metrics](#metrics) below). Unauthenticated; intended for scraping over a restricted network only. +### Email template retrieval + +`GET /email-template` returns the email template that pg-pkg has linked to the caller's API key. The key is validated through the same `Authorization: Bearer PG-…` flow the upload endpoints use, so no separate auth path is involved. + +| Case | Status | Body | +|---|---|---| +| Valid key with a template configured | `200` | `{ tenant_id, email_template }` | +| Missing or invalid key | `401` | error body | +| Valid key, no template configured | `404` | error body | +| PKG unreachable during validation | `503` | error body | + +The template is resolved on the PKG side and returned as-is, keyed on the validated tenant. + +[Source: api-description.yaml#L434-L465](https://github.com/encryption4all/cryptify/blob/0946c066a805f1dad73c3966fb70b7aba90fea35/api-description.yaml#L434-L465) + ### `POST /fileupload/init` request body | Field | Type | Required | Description | diff --git a/docs/repos/postguard.md b/docs/repos/postguard.md index e5fd906..f04f82b 100644 --- a/docs/repos/postguard.md +++ b/docs/repos/postguard.md @@ -143,7 +143,14 @@ cargo run --release --bin pg-pkg server \ |---|---|---| | `IRMA_SERVER` | Yivi/IRMA server URL | `https://is.yivi.app` | | `DATABASE_URL` | PostgreSQL connection string | none | -| `PKG_ALLOWED_ORIGINS` | Comma-separated CORS allowlist (e.g. `https://postguard.eu,https://postguard.nl`). `*` keeps the legacy any-origin behaviour and logs a warning at startup. Also available as `--allowed-origins`. | `*` | +| `PKG_ALLOWED_ORIGINS` | Comma-separated CORS allowlist (e.g. `https://postguard.eu,https://postguard.nl`). Required: the server refuses to start without it. Pass `*` to deliberately allow any origin (this logs a warning at startup). Also available as `--allowed-origins`. | required | +| `PKG_RATELIMIT_PER_SECOND` | Sustained request rate per client IP for the whole `/v2` scope. Also available as `--ratelimit-per-second`. | `10` | +| `PKG_RATELIMIT_BURST` | Burst allowance for the general `/v2` limit. Also available as `--ratelimit-burst`. | `50` | +| `PKG_RATELIMIT_SENSITIVE_PER_SECOND` | Sustained request rate per client IP for the key-issuing endpoints (`POST /start`, `GET /key`, `GET /key/{timestamp}`, `POST /sign/key`, `GET /api-key/validate`). Also available as `--ratelimit-sensitive-per-second`. | `2` | +| `PKG_RATELIMIT_SENSITIVE_BURST` | Burst allowance for the sensitive-endpoint limit. Also available as `--ratelimit-sensitive-burst`. | `10` | +| `PKG_RATELIMIT_DISABLED` | Set to `true` to build the limiter in permissive mode (every request passes). Use behind a trusted proxy that does its own rate limiting. Also available as `--ratelimit-disabled`. | `false` | +| `PKG_RATELIMIT_TRUST_FORWARDED_FOR` | Set to `true` to key the limiter on the rightmost `X-Forwarded-For` entry instead of the TCP peer address, so per-client limiting works behind a trusted reverse proxy. Off by default so a directly exposed PKG never trusts client-supplied headers. Also available as `--ratelimit-trust-forwarded-for`. | `false` | +| `PKG_EMAIL_ATTRIBUTE` | Attribute type carrying the email in API-key signing identities. Production keeps the default. Test environments set a test-scheme type (e.g. `irma-demo.sidn-pbdf.email.email`), since `pbdf.*` credentials cannot be issued outside production. Must match cryptify's `email_attribute` and the SDK's `emailAttributes`. Also available as `--email-attribute`. | `pbdf.sidn-pbdf.email.email` | | `RUST_LOG` | Log level (`debug`, `info`, `warn`, `error`) | none | ### Running the PKG Server @@ -255,6 +262,12 @@ Request body for signing keys: | `GET` | `/health` | Health check. | | `GET` | `/metrics` | Prometheus metrics. | +### Rate limiting + +The `/v2` scope is rate limited per client IP in two tiers. The general tier covers the whole scope; the sensitive tier covers the key-issuing endpoints (`POST /start`, `GET /key`, `GET /key/{timestamp}`, `POST /sign/key`, `GET /api-key/validate`) with a tighter limit, applied before authentication runs. Requests over the limit get `429 Too Many Requests` with a `Retry-After` header. The limits are global across worker processes and tunable through the `PKG_RATELIMIT_*` variables listed under [Environment Variables](#environment-variables). + +The limiter keys on the TCP peer address by default. When the PKG runs behind a trusted reverse proxy, set `PKG_RATELIMIT_TRUST_FORWARDED_FOR=true` so it keys on the rightmost `X-Forwarded-For` entry (the hop the trusted proxy appends) instead of the proxy's own address. Only the rightmost entry is trusted; anything left of it is client-supplied and spoofable. + ### Authentication The PKG supports two authentication methods: diff --git a/docs/sdk/js-encryption.md b/docs/sdk/js-encryption.md index 5d88435..8ca772d 100644 --- a/docs/sdk/js-encryption.md +++ b/docs/sdk/js-encryption.md @@ -126,6 +126,25 @@ What gets retried: 5xx responses, fetch-level network errors (`TypeError` from ` The same `retry` config governs downloads. See [Decryption — Retries and resumable downloads](/sdk/js-decryption#retries-and-resumable-downloads). +### Email attribute types + +The SDK uses the production `pbdf.sidn-pbdf.email` attribute types for recipient builders, key requests, and signing. Test setups that run against a different scheme can override these with `emailAttributes`: + +```ts +const pg = new PostGuard({ + pkgUrl: 'https://pkg.staging.postguard.eu', + cryptifyUrl: 'https://storage.staging.postguard.eu', + emailAttributes: { + email: 'irma-demo.sidn-pbdf.email.email', + domain: 'irma-demo.sidn-pbdf.email.domain', + }, +}); +``` + +[Source: types.ts#L7-L27](https://github.com/encryption4all/postguard-js/blob/854d5c06ea7c852e8ebb8517687d71f0349bfd51/src/types.ts#L7-L27) + +Leave `emailAttributes` unset for production. An override must match the PKG's `PKG_EMAIL_ATTRIBUTE` and cryptify's `email_attribute`, or finalize and key issuance reject the identity. + ## Resume an interrupted upload A long-running upload can be interrupted by a page refresh, tab crash, navigation away, or process restart. The SDK exposes two primitives for rehydrating an in-flight session from Cryptify rather than starting over: the `FileState` type and the `resumeUpload` function. @@ -249,6 +268,25 @@ const envelope = await pg.email.createEnvelope({ Call `.toBytes()` to get the encrypted data, or pass the `Sealed` object directly to `pg.email.createEnvelope()` for email integration. +## Prepare a signing session ahead of time + +With Yivi signing, `encrypt()` starts the disclosure session after the user acts. On iOS that is a problem: a Yivi Universal Link only opens the app when the navigation happens inside a genuine user gesture, and at tap time the app deep-link does not exist yet, so the tap falls back to Safari. + +`pg.prepareSign()` starts the Yivi session early. It returns `mobileUrl` (the app deep-link, resolved once Yivi shows its mobile button — put it on an `` so one tap opens the app), `keys` (resolves on disclosure), and `cancel()`. Pass the resolved keys to `encrypt()` via `signingKeys`; `encrypt()` then uses them directly and never starts a second session. + +```ts +// when the compose form becomes valid: +const prep = pg.prepareSign({ element: '#hidden-yivi', attributes: SIGN_ATTRS, includeSender: true }); +const href = await prep.mobileUrl; // put on the "Send" +// user taps the anchor -> Yivi app opens (one gesture) +const signingKeys = await prep.keys; // resolves after disclosure +await pg.encrypt({ files, recipients, sign, signingKeys }).upload({ notify }); +``` + +[Source: types.ts#L116-L145](https://github.com/encryption4all/postguard-js/blob/f8bd16b154b79b92bb086c2c088f10e79babb7db/src/types.ts#L116-L145) + +The disclosure is identity-bound (sender email plus optional attributes) and independent of the files and recipients, so the resolved keys are valid for whatever is ultimately encrypted. `sign` is still required alongside `signingKeys`: it supplies envelope metadata and the friendly-sender line when `includeSender` is set. `mobileUrl` only settles on mobile, where Yivi shows the app button; on desktop it stays pending, so race it with a timeout. + ## Error handling All encryption methods can throw: