This document describes the architecture of the Advanced Electronic Signature (AES) service at aes.veripath.co.uk and its relationship to the GP Booking App and Dental Booking App.
AES is a signing service — it provides cryptographic signing of prescriptions using PAdES-B-LTA compliant digital signatures. It does not store patient data. Patient records remain in the respective booking applications.
aes.veripath.co.uk
├── /admin* Admin dashboard, pharmacies, certificates, audit
├── /prescribe* Standalone prescription form (dev/internal use)
├── /api/sign Server-to-server signing API (for booking apps)
├── /api/medicines/search Drug autocomplete (dm+d data)
└── /auth/* Keycloak OAuth2 (SSO)
Internal microservices (Docker):
| Service | Role | Internal URL |
|---|---|---|
aes-portal |
FastAPI web app + API gateway | port 8004 |
aes-pki |
Private PKI — certificate lifecycle | port 8001 |
aes-tsa |
RFC 3161 Timestamp Authority | port 8002 |
aes-signer |
PDF signing orchestrator (pyHanko) | port 8003 |
aes-softshsm |
SoftHSM2 key storage | internal |
All services authenticate through a single Keycloak instance (auth.veripath.co.uk):
| Client | Type | Purpose |
|---|---|---|
aes-portal |
Confidential (OAuth2) | Admin UI login for AES developers |
gp-booking-app |
Confidential (OAuth2) | Booking app user login |
aes-signer-sa |
Service account | Booking app → AES API calls |
aes-pki-sa |
Service account | Internal PKI operations |
aes-tsa-sa |
Service account | Internal timestamp operations |
A separate Keycloak realm (partners) is used for client organisations. Each organisation is a Group within this realm:
partners realm (Keycloak)
└── Group: "maple-surgery" { org_slug: "maple-surgery" }
└── Group: "oak-practice" { org_slug: "oak-practice" }
└── Group: "dental-clinic-1" { org_slug: "dental-clinic-1" }
Keycloak protocol mappers add the org_id claim to each user's JWT token based on group membership.
Booking App AES Portal Keycloak
│ │ │
│ 1. User logs in │ │
│ ────────────────────────────────────────────────────────▶│
│◀─────────────────────────────────────────────────────────│
│ JWT: { user, org_id } │ │
│ │ │
│ 2. Clinician views │ │
│ patient record │ │
│ (in booking app DB) │ │
│ │ │
│ 3. Clicks "Issue │ │
│ Prescription" │ │
│ │ │
│ 4. POST /api/sign │ │
│ Authorization: │ │
│ Bearer <SA token> │ │
│ { │ │
│ org_id: "maple", │ │
│ clinician_cn: .. │ │
│ patient: { │ (from booking app DB) │
│ name, nhs_no,.. │ │
│ }, │ │
│ medication: {...}, │ │
│ pharmacy_id: 123 │ │
│ } │ │
│─────────────────────────▶│ │
│ │ 5. Validate JWT │
│ │ 6. Call signer → TSA → PKI │
│ │ 7. Store ref (NO patient │
│ │ data saved in AES) │
│◀─────────────────────────│ │
│ { success: true, │ │
│ prescription_id, │ │
│ signed_pdf_path } │ │
│ │ │
│ 8. Store prescription │ │
│ reference in │ │
│ booking app DB │ │
The /prescribe form remains accessible for development and testing, with full patient data entry.
| Data | Stored In | Notes |
|---|---|---|
| Patients | Booking App DB | Source of truth. Shared across apps. |
| Clinical notes | Booking App DB | Linked to patient records |
| Prescriptions (metadata) | AES portal.db |
org_id, prescription_id, clinician, pharmacy, status, PDF path. No patient data. |
| Signed PDFs | AES filesystem (/data/prescriptions/) |
PAdES-B-LTA compliant, contains patient data embedded in PDF |
| Pharmacies | AES portal.db |
Shared across all organisations (NHS directory) |
| Medications | AES portal.db |
dm+d VMP data (24,511 entries) |
| Audit logs | AES filesystem (/data/audit/) |
Tamper-evident, includes org_id |
| Certificates | AES PKI + filesystem (/data/certs/) |
HSM-backed keys |
portal_prescriptions:
- id (PK)
- prescription_id (unique, from booking app)
- org_id (from JWT claim)
- clinician_cn
- clinician_gmc
- medication_name
- medication_details (JSON)
- pharmacy_id (FK → pharmacies)
- status
- signed_pdf_path
- delivery_status
- signed_at
- created_at
Patient data is NOT stored in this table. It exists only in the signed PDF and the booking app's database.
POST /api/signServer-to-server endpoint for booking applications to create signed prescriptions.
Authentication: Bearer JWT (Keycloak service account token)
Request:
{
"prescription_id": "RX-20260519-0001",
"org_id": "maple-surgery",
"clinician_cn": "Dr Jane Smith",
"patient": {
"name": "John Smith",
"nhs_number": "1234567890",
"dob": "1980-01-15",
"address": "123 High Street"
},
"medication": {
"name": "Amoxicillin 500mg capsules",
"dosage": "One three times daily",
"quantity": "21",
"form": "Capsules",
"strength": "500mg"
},
"pharmacy_id": 123,
"instructions": "Take with food"
}
Response:
{
"success": true,
"prescription_id": "RX-20260519-0001",
"org_id": "maple-surgery",
"signed_pdf_path": "/data/prescriptions/RX-20260519-0001.pdf",
"signed_at": "2026-05-19T10:00:00+00:00",
"delivery_status": "pending"
}
GET /api/medicines/search?q=<query>Drug autocomplete for prescription forms.
Response:
{
"results": [
{"vpid": "123", "name": "Amoxicillin 500mg capsules", "abbrev": "Amox 500mg caps"}
]
}
┌──────────────┐ TLS 1.2 ┌──────────────┐
│ Internet │────────────────▶│ nginx │
└──────────────┘ └──────┬───────┘
│
┌─────────▼─────────┐
│ aes-portal:8004 │
│ (FastAPI) │
└──┬──────┬──────┬───┘
│ │ │
┌──────────────────┘ │ └──────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ aes-pki │ │ aes-signer │ │ aes-tsa │
│ port 8001 │ │ port 8003 │ │ port 8002 │
└──────┬───────┘ └──────────────┘ └──────────────┘
│
▼
┌──────────────┐
│ aes-softshsm │
│ (HSM keys) │
└──────────────┘
For the GP Booking App (and Dental Booking App) to use AES for prescription signing, the following changes are needed:
aes-signer-sa service account to the booking app for API authenticationPOST aes-portal:8004/api/sign when a clinician issues a prescriptionprescription_id and signed_pdf_path against the patient recordThe 21,100 imported pharmacies have no delivery endpoints configured. For each pharmacy that should receive electronic prescriptions:
delivery_endpoint (HTTPS URL) or delivery_email| Requirement | How Met |
|---|---|
| Data segregation by org | org_id on every record; app enforces org-scoped queries |
| Patient data minimisation | AES stores no patient data — only prescription references |
| Audit trail | All signing events logged with org_id, clinician ID, timestamp |
| Encryption at rest | Signed PDFs stored on encrypted filesystem |
| Encryption in transit | TLS 1.2+ for all external; internal network isolated |
| Access control | Keycloak SSO with role-based access per organisation |