Cambodia certificate QR + Verification
How LUCT Cambodia CMS Registry generates certificate QR codes via the external Limkokwing QR SaaS, and how public scans land on the Verification site. Evidence from Vara’s CMS pack (7 Oct 2026) and QR / Verification Drive packs (8 Oct 2026). M5 campus integration — not Lesotho M4 Applicant Demo letters.
1. Summary
Certificate QR is not generated inside Portal API or the Applicant Demo. Cambodia Registry PHP calls an external QR registry; the QR payload is a URL into the public Verification app. Vara (8 Oct): “Do check in CMS Cambodia, there's a call to the QR system to generate the QR code.”
Three systems
| System | Role | Host / path |
|---|---|---|
| Cambodia CMS (Registry) | Builds short_code + verify URL; POSTs create API; saves PNG | cambodia/registry/qrcode_gen*.php (campus SoR) |
| QR System (SaaS) | Registers QR identity; returns base64 PNG | https://qr.limkokwing.net/api/create_qrcode_api |
| Verification site | Public scan target; shows cert validity for campus path | https://verification.limkokwing.net/cambodia/{token} |
Standup actions (7–8 Oct 2026)
| Owner | Action | Status |
|---|---|---|
| Mohd. Paramasvara | Send Cambodia QR-system code | Done — Drive QR System + Verification (WhatsApp 8 Oct) |
| Iman Suherman | Review QR system → update CMS documentation | Done (CMS→QR) — this page + Markdown; QR/Verification DB inventory when local |
2. Runtime flow
Observed from current-style qrcode_gen.php (Mar 2026). Inputs arrive as
query params: StdProgramID, StudentID, ProgramCode.
Issue (Registry) then scan (public)
Create stores the Verification URL in the QR DB, but the PNG itself encodes the short SaaS URL. Scans hit QR → HTTP redirect → Verification.
sequenceDiagram actor Staff as Registry staff participant REG as qrcode_gen.php participant QR as qr.limkokwing.net CI4 participant VER as verification.limkokwing.net participant CMS as campus2_cambodia participant FILE as StudentID_ProgramCode.png Staff->>REG: StdProgramID · StudentID · ProgramCode REG->>REG: short_code = KHM-PNH_ + StdProgramID REG->>REG: token = AES-256-CBC(StdProgramID) REG->>QR: POST /api/create_qrcode_api Note over REG,QR: name · category=LUCT · short_code
url = verification…/cambodia/token QR->>QR: Insert qrcode.slug + url QR->>QR: PNG data = qr…/qrcode/short_code QR-->>REG: JSON Qrcode base64 PNG REG->>FILE: download PNG for certificate print actor Public as Scanner Public->>QR: GET /qrcode/KHM-PNH_… QR-->>Public: 302 → verification…/cambodia/token Public->>VER: GET /cambodia/token VER->>VER: AES decrypt → StdProgramID VER->>CMS: CALL GetStudentCertInfo(StdProgramID) CMS-->>VER: name · programme · CertSN · blob VER-->>Public: Verified HTML page
Issue checklist (Registry)
- Build
short_code = KHM-PNH_{StdProgramID}. - Encrypt
StdProgramID→ base64 token; build Verification URL under/cambodia/. - POST create API (see §3). SaaS stores
url; PNG encodes short redirect URL. - Parse JSON field
Qrcode(base64 PNG); save as{StudentID}_{ProgramCode}.png.
2b. Manual certificate ops (not in CMS code)
Standup 8 Oct (Dexter): Cambodia ↔ Cyberjaya still exchanges printed certificates / QR packs /
student data outside qrcode_gen*. Do not invent a Portal API
courier method. Automated create/scan remains M5; this swimlane is ops documentation only.
Cambodia ↔ Cyberjaya handoff
sequenceDiagram
participant KH as Cambodia campus
participant CY as Cyberjaya ops
Note over KH,CY: Manual — not modelled in Registry PHP
KH->>CY: Certificates · QR PNGs · student data pack
CY->>CY: Print / stamp / process
CY-->>KH: Return physical certificates
Note over KH: Public scan still Verification → GetStudentCertInfo
Combined standup write-up (Applicant merge + fee SPs): fee-payment-sps.html · Markdown applicant-qr-fee-mechanisms.md.
3. Create API contract
Endpoint
| Item | Value |
|---|---|
| Method | POST |
| Current host | https://qr.limkokwing.net/api/create_qrcode_api |
| Older host (variants) | https://qrcode.limkokwing.net/api/create_qrcode_api |
| Body | application/x-www-form-urlencoded (PHP cURL POSTFIELDS array) |
| TLS in scripts | CURLOPT_SSL_VERIFYPEER/HOST = false — do not copy into Raisd Live adapters |
| Ops note | Comments mention Cloudflare challenges on the QR host; staff sometimes copy a debug cURL |
Request fields
| Field | Example / rule | Notes |
|---|---|---|
name |
Verification Cambodia Cert - {StdProgramID} |
Newer script makes name unique per serial; older uses fixed label |
category |
LUCT |
Institution bucket in QR SaaS |
subcategory |
Certificate |
Accepted by API but not persisted in SaaS DB (category only) |
short_code |
KHM-PNH_{StdProgramID} |
Campus + city prefix + programme serial |
url |
https://verification.limkokwing.net/cambodia/{token} |
This is what the QR encodes — not raw student PII |
created_by |
User |
Literal in observed scripts |
Response (from QR System ApiController)
- JSON; field
Qrcode= base64 PNG. Alsostatus(1 success / 0 error-or-exists) andmessage. - If
short_codealready exists: returns existing PNG withstatus: 0, message “QR code already exists” (CMS still gets an image). - PNG payload data =
{base_url}/qrcode/{short_code}, not the Verification URL. Redirect resolves the storedurl. - Image via Endroid QR Code (300px, medium ECC). Stored under
writable/uploads/qrcodes/{short_code}.png. - Category auto-created by name if missing (description “Auto-created category”) — how
LUCTappeared in the dump.
4. QR System pack (inventoried)
Pack folder qr.limkokwing.net/qrcode-lkwn — CodeIgniter 4 app starter
(codeigniter4/appstarter, PHP ≥ 8.1, libraries
endroid/qr-code + khanamiryan/qrcode-detector-decoder.
Dump file qrcode.sql (MariaDB 10.11, generated 8 Oct 2026, DB name qrcode).
Admin UI routes (session auth)
| Path | Controller | Notes |
|---|---|---|
/, login / logout / forgot / reset | Qrsystem | auth filter on dashboard |
/categories, add/edit | CategoryController | Admin CRUD |
/users, add/edit | UserController | Admin / SuperAdmin |
/add-new-qr, /edit-qrcode/{slug} | QrCodeController | Manual QR entry |
/qrcode/{slug} | QrCodeController::redirect | Public 302 → stored URL |
/qrcode-image/{slug} | get_image | Serves PNG |
POST /api/create_qrcode_api | ApiController | No auth filter in Routes — campus integration entry |
POST /api/qrcodes, delete/*, check-slug, … | ApiController | Admin AJAX helpers |
Database qrcode (structure)
| Table | Role | Key columns |
|---|---|---|
qrcode | QR registry | name, slug (= short_code), category_id, url, qr_image_file |
categories | Buckets | category_name, description |
users | Admin accounts | username, bcrypt password, accesslevel Admin|SuperAdmin — hashes not published |
forgetpass | Reset tokens | token, user_id |
migrations | CI4 | framework bookkeeping |
Dump snapshot (counts — no PII republished)
| Metric | Value |
|---|---|
Total qrcode rows (parsed) | ~697 |
Cambodia certs (KHM-PNH_* → Verification) | 685 (category LUCT) |
| Other QR uses | Namecards (LUCT / CCC / Raisd), Events, Publicity, General intake |
| Categories in dump | General · Events · Publicity · Namecard LUCT · Namecard CCC · Namecard Raisd · LUCT |
| Other campus short-code prefixes in dump | None — only KHM-PNH_ among certificate-style slugs |
The QR SaaS is therefore a multi-purpose Limkokwing short-link / QR registry (namecards + marketing + certificates), not Cambodia-only. Certificate volume dominates this dump.
5. Verification pack (inventoried)
Zip verification.limkokwing.net_source_code_08_10_2026.zip is small (~13 KiB):
a front controller, crypto helper, Cambodia cert model, Apache rewrite, and docker stubs.
No full framework tree.
Request path
flowchart TD
A["GET /{country}/{token}"] --> B[urldecode token]
B --> C[AES-256-CBC decrypt]
C --> D[StdProgramID]
D --> E["CALL GetStudentCertInfo(StdProgramID)"]
E --> F{Row found?}
F -->|yes| G[Certificate Verified UI]
F -->|no| H[No certificate found]
- Apache
.htaccessrewrites all non-files toindex.php. - First path segment is treated as
country(UI hard-codes campus label “Cambodia” today). - Decrypt supports plain numeric
StdProgramIDor legacy…_timestamp_dateforms (takes trailing segment after last_when non-numeric).
CMS stored procedure GetStudentCertInfo
Defined on campus2_cambodia (CMS procedures dump 7 Oct 2026). Verification calls it over MySQL
with a dedicated DB user (credentials in pack — ops secret, not published).
- Input:
StdProgID(varchar). - Reads
r_studentcert_cambodia+ joins_uploadwhereUploadType in (11)for cert image blob. - Returns:
StdProgramID,StudentID,StudentName,PicURL,ProgramCode,ProgramName,GraduationDate,CertSN,CertFileBlob,CertFileName,CertFileType.
Verified page fields
- Student photo (
PicURL), name, Student ID - Programme name · Campus label “Cambodia” · Graduation date
- Certificate serial (
CertSN) · inline certificate PNG from blob when SN present - Footer branding: “Raisd AI”
qrcode_gen.php and Verification lib/Encryption.php use
different AES key/IV material (fingerprints differ). Live systems must keep encrypt/decrypt
keys aligned; Raisd adapters should take secrets from vault, not from checked-in dumps.
6. Verify token & short code
Public Verification URL
https://verification.limkokwing.net/cambodia/{token}
{token}= base64(AES-256-CBC ciphertext ofStdProgramID).- URL-encoded when appended (
urlencode/rawurlencodedepending on script). - Stored as
qrcode.urlin the SaaS. The PNG encodesqr.limkokwing.net/qrcode/{short_code}, which 302-redirects here. - Key material in Registry + Verification PHP — ops secret; never republish. Packs currently disagree (see §5).
Short-code convention
| Part | Value | Meaning |
|---|---|---|
| Country | KHM | Cambodia (ISO-ish campus code) |
| City / campus | PNH | Phnom Penh |
| Separator | _ | |
| Serial | StdProgramID | Student–programme row id from CMS |
Raisd should treat short-code allocation as owned by the QR SaaS + campus convention — do not invent a parallel allocator in Schema without campus agreement.
Crypto evolution (high level)
- Current path: OpenSSL
AES-256-CBCofStdProgramIDalone (commented older path appended_timestamp_YYYYMMDD). - Legacy:
qrcode_gen1.php/ early helpers used mcrypt Rijndael — obsolete; not the create-API path. - Local-only QR: early
qrcode.phpbuilt images withphpqrcodeagainst Verification base URL — superseded by SaaS create API.
7. CMS Registry scripts
All under cambodia/registry/ in the CMS pack
(CMS_Cambodia_Source_Codes_&_DB_Structure_07_10_2026). Dates from file mtimes in the extract.
| File | ~Date | QR host | Role |
|---|---|---|---|
qrcode_gen.php |
Mar 2026 | qr.limkokwing.net |
Current-style — unique name, create API, PNG download, debug comments |
qrcode_gen_onthespot.php |
Jun 2024 | qr.limkokwing.net |
On-the-spot cert path — same API shape |
qrcode_gen_18092025.php |
Jul 2025 | qr.limkokwing.net |
Dated variant — same API shape |
qrcode_gen4.php |
Jul 2025 | qrcode.limkokwing.net |
Older hostname; requires all three GET params |
qrcode_gen3.php |
Jul 2025 | qrcode.limkokwing.net |
Sample hard-coded verify URL (debug residue) |
qrcode.php |
Oct 2023 | local phpqrcode |
Early helper — encrypt + local image; not SaaS create |
qrcode_gen1.php |
Oct 2023 | — | mcrypt encrypt/decrypt smoke test only |
Output artefact
Filename pattern: {StudentID}_{ProgramCode}.png
(example defaults in the current script: 601001001_PUFP.png).
Served as Content-Disposition: attachment, then unlinked from the web root.
8. Hosts & source packs
External hosts
| Host | Purpose |
|---|---|
qr.limkokwing.net |
Current QR registry / create API (+ admin UI presumed in QR System pack) |
qrcode.limkokwing.net |
Legacy/alternate create API host still present in older scripts |
verification.limkokwing.net |
Public Verification app; Cambodia path /cambodia/{token} |
Drive packs (outside git)
| Pack | Shared | Local dump | Status for inventory |
|---|---|---|---|
| CMS Cambodia code + DB | 7 Oct 2026 | raisd/_local/cambodia-cms-2026-10-07/ |
Done — Registry call site extracted |
| QR System Source Code & Database | 8 Oct 2026 | raisd/_local/cambodia-qr-2026-10-08/qr-system/ |
Done — CI4 + qrcode.sql (§4) |
| Verification Source Code | 8 Oct 2026 | …/verification/ |
Done — front controller + model (§5) |
9. Raisd implications
- Campus integration, not Portal API core (Lesotho M4). Applicant Demo DEMO PDFs must not pretend to be this certificate QR path.
-
Three hops on scan:
PNG → QR short URL → Verification → live
campus2_cambodiaviaGetStudentCertInfo. Raisd Live must not break the SP or upload-type 11 cert blob join. -
QR SaaS is shared infrastructure (namecards + marketing + certs).
This dump’s certificate short-codes are Cambodia-only (
KHM-PNH_); other campuses may adopt new prefixes later. - M5 adapter: after official cert issuance, POST create API (TLS verify on, secrets in vault). Expect idempotent “already exists” responses. Prefer documenting Cloudflare allow-lists over disabling SSL verification.
- AES keys must match between encrypt (Registry/adapter) and decrypt (Verification). Do not ship pack keys into git; resolve live key ownership with campus ops.
- Verification DB access is a direct MySQL read of CMS — treat as high-trust path (network allow-list, least-privilege user, no credentials in repos).
Architecture sketch (current + Raisd Live)
flowchart LR
subgraph Issue["Issue path"]
REG[Registry qrcode_gen]
ADAPT[Future Raisd ACL]
end
subgraph QRSaaS["qr.limkokwing.net"]
API[create_qrcode_api]
REDIR["/qrcode/slug → 302"]
end
subgraph Verify["verification.limkokwing.net"]
FRONT[index.php decrypt]
end
subgraph SoR["campus2_cambodia"]
SP[GetStudentCertInfo]
end
REG --> API
ADAPT -.-> API
API --> REDIR
REDIR --> FRONT
FRONT --> SP