Last updated: June 2026 — eIDAS compliance, booking app integration, evidence capture
The AES Portal is a standalone microservice architecture deployed at aes.veripath.co.uk. It provides PAdES-B-LTA digital signing for prescriptions, replacing the need for NHS EPS infrastructure with a self-contained, HSM-backed signing pipeline.
The system integrates with the GP Booking App (and Dental Booking App) via server-to-server API calls. Clinicians interact through their booking app's prescription form — not directly through the AES Portal, except for administration.
aes.veripath.co.uk
├── /admin Settings, certificates, audit, pharmacy management
├── /prescribe Standalone prescription form (dev/internal use)
├── /auth/* Keycloak OAuth2 authentication
├── /privacy Privacy notice
└── /api/
├── /sign Server-to-server signing (for booking apps)
├── /pharmacies Pharmacy search/lookup
└── /prescriptions/{id}
├── /pdf Download signed PDF with chain-of-custody
└── /evidence Download Evidence Summary (eIDAS compliance)
The system consists of six internal services running in a Docker Compose stack:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ aes-pki │────▶│ aes-tsa │ │ aes-signer │
│ (PKI mgr) │ │ (timestamp) │◀────│ (orchestr.) │
└──────┬───────┘ └──────────────┘ └──────┬───────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ aes-softshsm │ │ aes-portal │
│ (HSM keys) │ │ (web app) │
└──────────────┘ └──────┬───────┘
│
▼
┌──────────────┐
│ SQLite DB │
│ (prescriptions│
│ audit, │
│ pharmacies) │
└──────────────┘
The main web application. Serves the admin dashboard, prescription form, API endpoints for booking apps, and authentication flow. Communicates with aes-signer for signing operations and maintains the prescription records, audit chain, and pharmacy database.
New API endpoints (June 2026):
GET /api/prescriptions/{id}/pdf — Download signed PDF with chain-of-custody logging and SHA-256 hash headerGET /api/prescriptions/{id}/evidence — Download the eIDAS-compliant Evidence Summary JSONManages the certificate lifecycle:
Certificates are issued via REST API (POST /certificates/issue) and stored in a shared volume (/data/certs/).
Provides cryptographically verifiable timestamps for every signing operation. Essential for PAdES-B-LTA long-term validation. Each signed PDF contains an embedded TSA token proving the signature existed at a precise point in time.
The core signing engine:
Software-based Hardware Security Module. Stores all private keys (CA root, clinician, TSA) and performs signing operations without exposing private key material to any other service.
Persists prescription signing records (PortalPrescription model), pharmacy data, delivery logs, and references to signed PDFs. Patient data is NOT stored — only prescription metadata, actor evidence, and pharmacy information.
The GP Booking App acts as the clinician-facing interface. The AES Portal is called via API:
Booking App AES Portal Keycloak
│ │ │
│ 1. User logs in (MFA) │ │
│ ────────────────────────────────────────────────────────▶│
│◀─────────────────────────────────────────────────────────│
│ JWT: { user, org_id, │
│ acr (MFA level) } │
│ │ │
│ 2. Clinician fills │ │
│ prescription form │ │
│ ✓ Confirms consent │ │
│ statement │ │
│ │ │
│ 3. POST /api/sign │ │
│ Authorization: │ │
│ Bearer <USER JWT> │ ← clinician's own token │
│ X-Forwarded-For: │ ← real client IP │
│ User-Agent: <browser>│ ← real browser UA │
│ { prescription, │ │
│ consent } │ │
│─────────────────────────▶│ │
│ │ 4. Verify JWT, extract │
│ │ realm → fetch JWKS │
│ │ 5. Verify RSA signature │
│ │ 6. Capture actor evidence │
│ │ (sub, acr, IP, UA) │
│ │ ✓ Consent stored │
│ │ 7. Call signer → TSA → PKI │
│ │ 8. Compute PDF SHA-256 │
│ │ 9. Log to audit chain │
│◀─────────────────────────│ │
│ { success, │ │
│ prescription_id, │ │
│ signed_pdf_path, │ │
│ pdf_sha256 } │ │
│ │ │
│ 10. Store evidence in │ │
│ booking app DB │ │
Key improvements (June 2026):
python-requests)The /prescribe form remains accessible for development and testing, with full patient data entry directly in the AES Portal.
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│Clinician │ │ AES │ │ PKI │ │ TSA │ │ HSM │
│(browser) │ │ Portal │ │ Service │ │ Service │ │(SoftHSM) │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │ │
│ 1. Login via │ │ │ │
│ Keycloak SSO │ │ │ │
│─────────────────▶│ │ │ │
│ │ │ │ │
│ 2. Fill │ │ │ │
│ prescription │ │ │ │
│ form │ │ │ │
│─────────────────▶│ │ │ │
│ │ │ │ │
│ 3. Sign & Send │ │ │ │
│─────────────────▶│ │ │ │
│ │ 4. Get cert │ │ │
│ │───────────────▶│ │ │
│ │ │ 5. Sign with │ │
│ │ │ HSM key │ │
│ │ │───────────────────────────────▶│
│ │ │◀───────────────────────────────│
│ │◀───────────────│ │ │
│ │ │ │ │
│ │ 6. Sign PDF │ │ │
│ │────────────────────────────────▶ │
│ │ │ │ │
│ │ 7. Timestamp │ │ │
│ │───────────────────────────────────▶ │
│ │◀─────────────────────────────────── │
│ │ │ │ │
│ │ 8. Signed PDF │ │ │
│ │ + delivery │ │ │
│◀─────────────────│ │ │ │
Server-to-server endpoint for booking applications. Accepts clinician's JWT, prescription data, and consent information. Returns signed PDF metadata with SHA-256 hash.
Authentication change (June 2026): Previously used a service account token (
aes-signer-sa). Now accepts the clinician's own OIDC access token for eIDAS-compliant sole-control evidence. Falls back to service account if the user JWT is unavailable.
Request headers forwarded for evidence capture:
X-Forwarded-For — Clinician's real IP addressUser-Agent — Clinician's browser User-Agent stringDownload a signed PAdES-B-LTA PDF. Each download is logged to the hash-chained audit trail with actor identity, IP, and User-Agent (chain-of-custody).
Response header: X-Content-SHA256: <sha256> — the SHA-256 hash of the PDF for tamper verification.
Returns the full Evidence Summary JSON containing the seven data pillars required for eIDAS compliance (see Evidence Capture section below).
Pharmacy search with relevance-ordered results (prefix name matches first, then name contains, then address contains). Supports ?q= and ?search= parameters.
Every signing event captures the following evidence, stored both in the AES Portal's database (medication_details JSON) and the booking app's PostgreSQL (medication_details JSONB):
| Pillar | Field | Source |
|---|---|---|
| Cryptographic | pdf_sha256 |
SHA-256 hash of the signed PDF |
| Identity | actor_sub |
Keycloak user UUID (sub claim) |
actor_username |
Keycloak preferred_username | |
actor_acr |
Auth Context Ref (MFA level) | |
actor_auth_time |
MFA authentication timestamp | |
| Forensic | actor_ip |
X-Forwarded-For (real client IP) |
actor_user_agent |
Browser User-Agent string | |
| Intent | consent_text |
Signed consent statement |
consent_timestamp |
When consent was given | |
| Temporal | signed_at |
TSA-backed signing timestamp |
| Chain-of-Custody | audit_chain.jsonl | Hash-chained log of all events |
When a clinician toggles "Issue Immediately (AES sign on save)", a consent checkbox appears requiring explicit confirmation:
"I confirm I wish to electronically sign this prescription as the prescriber. I understand this is a legally binding Advanced Electronic Signature under the Human Medicines Regulations 2012."
The consent text and timestamp are stored in both the AES Portal and the booking app's database.
The AES Portal maintains two audit files in /data/audit/:
prev_hash linking to the preceding entry, forming a verifiable SHA-256 chain| Event | Data |
|---|---|
prescription_signed |
actor_sub, acr, IP, prescription_id, clinician, patient, medication |
prescription_pdf_downloaded |
Actor, IP, User-Agent, prescription_id, PDF SHA-256 |
evidence_summary_downloaded |
Actor, IP, prescription_id |
prescription_delivered |
Delivery method, prescription_id |
delivery_failed |
Error message, prescription_id |
system_startup |
Service lifecycle |
The chain integrity can be verified via the AES Portal Admin dashboard at /admin/audit.
Authentication is handled by Keycloak. The system supports multi-realm JWT verification — each clinic operates in its own realm, and the AES Portal dynamically fetches the correct JWKS based on the token's iss claim.
iss claim extracted: https://auth.veripath.co.uk/realms/{realm}GET /realms/{realm}/protocol/openid-connect/certsn and e parameters (RS256)sub, preferred_username, acr, auth_time| Client | Type | Purpose |
|---|---|---|
aes-portal |
Confidential (OAuth2) | Admin UI login (per-realm) |
gp-booking-app |
Confidential (OAuth2) | User login in each clinic realm |
aes-signer-sa |
Service account | Fallback API calls |
aes-pki-sa |
Service account | Internal PKI operations |
aes-tsa-sa |
Service account | Internal timestamp requests |
| Boundary | Protection |
|---|---|
| Internet → nginx | TLS 1.2+, Let's Encrypt certificate |
| nginx → aes-portal | Internal Docker network only (port 8001) |
| aes-portal → other services | JWT bearer tokens (Keycloak) |
| Internal services | No public exposure; isolated Docker network |
| Key storage | SoftHSM2, keys never leave the HSM boundary |
| Patient data | NOT stored in AES Portal — only prescription references |
| Evidence data | Stored in both AES Portal and booking app PostgreSQL |
The GP Booking App (Django) integrates via the AESClient class (clinical_data/integrations/aes_client.py):
AESClient.sign_prescription() — sends the clinician's JWT, prescription data, consent text, client IP, and browser UA to the AES PortalAESClient.download_prescription_pdf() — fetches the signed PAdES-B-LTA PDF with SHA-256 hash headerAESClient.download_prescription_evidence() — fetches the eIDAS Evidence Summary JSONThe booking app stores AES metadata in the clinical_data_prescription.medication_details JSONB field, making all evidence queryable via direct PostgreSQL access.