You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: replace static iOS cert embedding with SCEP for on-device key generation
iOS/macOS profiles now use a SCEP payload instead of a bundled PKCS#12.
PINT acts as a SCEP Registration Authority: it issues a one-time challenge
at profile download time, validates it when the device submits its CSR,
and proxies the signing request to FreeIPA. iOS handles automatic renewal
before the 1-year pint_wifi cert expires, so users install the profile once
and never touch it again.
- Add internal/scep: ChallengeStore (15-min one-time tokens), RA cert
generation/parsing (self-signed RSA 2048, never expires), and SCEP HTTP
handler (GetCACaps, GetCACert, PKIOperation with GET fallback)
- Replace PKCS#12 identity payload in mobileconfig with com.apple.security.scep
- Revert iOS cert path in main.go back to EC marshaling for RadSec/signing certs
- Add Caddy to Procfile (only starts when PINT_SERVER_URL=https://localhost:8443)
- Wire PINT_SCEP_RA_CERT_SECRET through Helm chart (helper, values, deployment, role)
- Update pint_wifi profile: 1-year validity, accept RSA and EC keys
- Update profile page: "1 year" validity, Manual Configuration section for
Android/Windows/Linux, collapsible SCEP explainer for iOS/macOS
- Add date to dev log timestamps
- Update README: dual cert-gen paths with sequence diagrams, SCEP RA notes,
Testing SCEP Locally section
Copy file name to clipboardExpand all lines: README.md
+69-16Lines changed: 69 additions & 16 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -23,36 +23,65 @@ flowchart TD
23
23
24
24
### Certificate Generation via FreeIPA
25
25
26
-
Every certificate PINT issues follows the same path: generate a secp384r1 ECDSA keypair locally, build a CSR, and call FreeIPA's `cert_request` RPC with the appropriate CA and profile. FreeIPA's Dogtag CA signs the cert and returns the DER-encoded result. The private key never leaves PINT; it is either bundled into the download or shown once and discarded.
26
+
PINT uses two distinct paths to issue WiFi client certificates depending on the platform.
27
+
28
+
**iOS and macOS — SCEP (on-device key generation)**
29
+
30
+
PINT acts as a SCEP Registration Authority. The device generates its own RSA 2048 keypair locally and enrolls through the `/scep` endpoint embedded in the mobileconfig. The private key never leaves the device, and iOS handles automatic renewal before the certificate expires.
31
+
32
+
```mermaid
33
+
sequenceDiagram
34
+
participant iOS as iOS / macOS Device
35
+
participant P as PINT
36
+
participant IPA as FreeIPA (Dogtag)
37
+
38
+
iOS->>P: Download profile (authenticated)
39
+
P->>P: Issue one-time SCEP challenge
40
+
P-->>iOS: Mobileconfig with SCEP payload + challenge
41
+
note over iOS: Profile installed; device generates RSA 2048 keypair
note over iOS: Certificate installed; WiFi connects automatically
48
+
note over iOS,P: Near expiry: iOS re-runs SCEP automatically
49
+
```
50
+
51
+
The SCEP Registration Authority (RA) uses a self-signed RSA 2048 certificate (CN `CSH PINT SCEP RA`) stored in the `pint-scep-ra-cert` Kubernetes Secret. PINT generates this automatically on first startup if the Secret does not exist. It is intentionally self-signed and set to never expire. The RA cert is not a trust anchor; it is only used to encrypt the CMS envelope during the SCEP exchange. iOS identifies it via the SHA-1 fingerprint embedded in the mobileconfig's SCEP payload.
52
+
53
+
**All other platforms — server-side key generation**
54
+
55
+
For Android, Windows, and Linux, PINT generates a secp384r1 ECDSA keypair, submits the CSR to FreeIPA's `cert_request` RPC, and bundles the result into a PKCS#12 archive for download. The private key is shown once and never stored.
27
56
28
57
```mermaid
29
58
sequenceDiagram
30
-
participant Client as Browser / CLI
59
+
participant Client as Browser
31
60
participant P as PINT
32
61
participant IPA as FreeIPA (Dogtag)
33
62
34
-
Client->>P: Request enrollment
63
+
Client->>P: Request profile download
35
64
P->>P: Generate secp384r1 keypair + CSR
36
65
P->>IPA: cert_request(CSR, principal, CA, profile)
37
66
IPA-->>P: Signed certificate (DER)
38
-
P->>P: Bundle into profile / p12 / PEM
67
+
P->>P: Bundle into .p12 / .xml
39
68
P-->>Client: Download
40
69
```
41
70
42
71
PINT authenticates to FreeIPA using a service account specified by `PINT_IPA_SERVICE_ACCOUNT` and `PINT_IPA_PASSWORD`. The session is established at startup and re-authenticated automatically on 401.
43
72
44
73
#### Profiles
45
74
46
-
Three custom Dogtag certificate profiles control validity, key usage, and subject enforcement. All profiles force `O=CSH.RIT.EDU` in the issued certificate subject regardless of what the CSR contains, and all require secp384r1 EC keys.
75
+
Three custom Dogtag certificate profiles control validity, key usage, and subject enforcement. All profiles force `O=CSH.RIT.EDU` in the issued certificate subject regardless of what the CSR contains.
47
76
48
-
| Profile | Purpose | Validity | EKU |
49
-
|---|---|---|---|
50
-
|`pint_wifi`| EAP-TLS client certs for member devices |5 years|`clientAuth`|
51
-
|`pint_radsec_client`| mTLS client certs for WiFi controllers | 5 years |`clientAuth`|
52
-
|`pint_radsec_server`| mTLS server cert for the FreeRADIUS RadSec listener | 90 days |`serverAuth`|
53
-
|`pint_profile_signing`| CMS signing cert for iOS mobileconfig profiles | 1 year |`codeSigning`|
77
+
| Profile | Purpose | Validity |Key |EKU |
78
+
|---|---|---|---|---|
79
+
|`pint_wifi`| EAP-TLS client certs for member devices |1 year | RSA 2048 or EC|`clientAuth`|
80
+
|`pint_radsec_client`| mTLS client certs for WiFi controllers | 5 years |EC (secp384r1) |`clientAuth`|
81
+
|`pint_radsec_server`| mTLS server cert for the FreeRADIUS RadSec listener | 90 days |EC (secp384r1) |`serverAuth`|
82
+
|`pint_profile_signing`| CMS signing cert for iOS mobileconfig profiles | 1 year |EC (secp384r1) |`codeSigning`|
54
83
55
-
Five-year validity on client certs minimises re-enrollment burden. The 90-day server cert and 1-year profile signing cert are automatically renewed by PINT (see [RadSec Server Cert](#radsec-server-cert) and [Profile Signing Cert](#profile-signing-cert)).
84
+
The `pint_wifi` profile accepts both RSA and EC keys because iOS generates RSA 2048 on-device via SCEP while other platforms submit EC keypairs generated by PINT. The 1-year validity is short enough to rotate credentials regularly while remaining transparent to users on iOS/macOS, where SCEP handles renewal automatically. The 90-day server cert and 1-year profile signing cert are also automatically renewed by PINT (see [RadSec Server Cert](#radsec-server-cert) and [Profile Signing Cert](#profile-signing-cert)).
56
85
57
86
Profile config files live in `ipa/profiles/`. They must be imported into FreeIPA once before PINT can use them. Use `ipa/update_profile.py`, which supports three actions:
58
87
@@ -82,11 +111,11 @@ Members visit `/profile` and download a platform-specific package. PINT issues a
82
111
83
112
| Platform | Output | Contents |
84
113
|---|---|---|
85
-
| iOS / macOS |`.mobileconfig` (Apple Configuration Profile) |PKCS#12 identity, WiFi CA, root CA, 802.1X/EAP-TLS config; optionally CMS-signed |
114
+
| iOS / macOS |`.mobileconfig` (Apple Configuration Profile) |SCEP payload, WiFi CA, root CA, code-signing CA, 802.1X/EAP-TLS config; optionally CMS-signed |
86
115
| Android |`.p12` (PKCS#12) | Client cert + key + WiFi CA, imported via Android WiFi settings |
87
116
| Windows |`.xml` (WLAN profile) + `.p12` (PKCS#12) | EAP-TLS config and CA thumbprint; cert imported separately into the Windows certificate store |
88
117
89
-
The iOS mobileconfig always embeds the WiFi intermediate CA and root CA so the full trust chain is installed in one step. When `PINT_IPA_CODE_SIGNING_CA_NAME` is set, PINT also embeds the code-signing intermediate CA and wraps the profile in a CMS `SignedData` envelope, letting iOS display it as "Verified" after the CA profile is trusted.
118
+
The iOS mobileconfig always embeds the WiFi intermediate CA and root CA so the full trust chain is installed in one step. The SCEP payload instructs iOS to generate a keypair on-device, enroll with PINT's `/scep` endpoint using a one-time challenge, and renew automatically near expiry — users never need to re-download the profile. When `PINT_IPA_CODE_SIGNING_CA_NAME` is set, PINT also embeds the code-signing intermediate CA and wraps the profile in a CMS `SignedData` envelope, letting iOS display it as "Verified" after the CA profile is trusted.
90
119
91
120
### WiFi Controller Enrollment
92
121
@@ -315,6 +344,7 @@ Set `envSecret` in your values to the name of this Secret. The chart mounts it v
315
344
| `helm` | Deploy the chart into kind |
316
345
| `kubectl` | Interact with the dev cluster |
317
346
| `overmind` | Run the Procfile (PINT + FreeIPA stub simultaneously) |
347
+
| `caddy` | HTTPS reverse proxy for local SCEP testing (iOS requires TLS) |
`make dev` starts two processes via `overmind` and the `Procfile`:
365
+
`make dev` starts three processes via `overmind` and the `Procfile`:
336
366
337
367
- **`ipa-stub`**: FreeIPA stub server on `:8088` (see [FreeIPA Stub](#freeipa-stub) below).
338
368
- **`pint`**: the PINT server on `:8080`. It waits for the stub to be ready before starting.
369
+
- **`caddy`**: HTTPS reverse proxy from `https://localhost:8443` to `http://localhost:8080`. Only starts when `PINT_SERVER_URL=https://localhost:8443` (see [Testing SCEP Locally](#testing-scep-locally) below); otherwise it idles.
339
370
340
371
FreeRADIUS runs in the kind cluster and persists between `make dev` sessions. PINT talks to it via the Kubernetes API using your local `~/.kube/config`.
341
372
@@ -392,10 +423,31 @@ All configuration is via environment variables. Copy `.env.dev.example` to `.env
392
423
| `PINT_RADIUS_STATUS_PORT` | `18121` | FreeRADIUS status server port |
393
424
| `PINT_RADIUS_RADSEC_CHECK_CRL` | `true` | Enable CRL checking in the RadSec TLS listener |
394
425
| `PINT_RADIUS_RADSEC_PROXY_PROTOCOL` | `false` | Expect HAProxy PROXY protocol header on RadSec connections; set `true` when HAProxy fronts FreeRADIUS |
426
+
| `PINT_SCEP_RA_CERT_SECRET` | `pint-scep-ra-cert` | K8s Secret for the SCEP Registration Authority (RA) certificate and key; auto-generated on first startup |
| `PINT_DISABLE_OIDC` | `false` | Bypass OIDC and inject a static dev user |
397
429
| `PINT_DEV_RTP` | `false` | Inject `rtp` group into dev user (requires `PINT_DISABLE_OIDC=true`) |
398
430
431
+
### Testing SCEP Locally
432
+
433
+
iOS requires HTTPS to complete SCEP enrollment, so local testing needs a TLS frontend. Caddy handles this automatically when you set `PINT_SERVER_URL` to the local HTTPS address in `.env.dev`:
434
+
435
+
```bash
436
+
# .env.dev
437
+
PINT_SERVER_URL=https://localhost:8443
438
+
PINT_DISABLE_OIDC=true # skip OIDC so you can download profiles without a Keycloak session
439
+
```
440
+
441
+
With those two variables set, `make dev` will start Caddy alongside PINT. On first run, Caddy generates a local CA and certificate. Trust it system-wide so the iOS profile installer accepts the mobileconfig:
After that, download a profile from `https://localhost:8443/profile`, install it on a device on the same network, and watch PINT's logs for the SCEP `PKIOperation` request and the resulting cert issuance. The SCEP Registration Authority (RA) certificate is auto-generated and stored in the `pint-scep-ra-cert` Kubernetes Secret on first startup; it persists across restarts.
450
+
399
451
### FreeIPA Stub
400
452
401
453
The stub (`dev/freeipa-stub/`) is a minimal HTTPS server that implements just enough of the FreeIPA JSON-RPC API for PINT to function locally. It runs on `:8088` with a self-signed TLS certificate, so `PINT_IPA_SKIP_TLS_VERIFY=true` must be set in `.env.dev`.
@@ -433,7 +485,8 @@ The stub maps profile IDs to EKU and validity:
433
485
|---|---|---|---|
434
486
|`pint_radsec_server`|`serverAuth`| 90 days | DNS SAN set to CSR CN (required for Go TLS verification) |
435
487
|`pint_profile_signing`|`codeSigning`| 1 year | Only available when `PINT_IPA_CODE_SIGNING_CA_NAME` is set |
436
-
| all others (`pint_wifi`, `pint_radsec_client`, …) | `clientAuth` | 5 years | |
488
+
|`pint_wifi`|`clientAuth`| 1 year | Accepts RSA and EC public keys |
489
+
|`pint_radsec_client` and others |`clientAuth`| 5 years ||
437
490
438
491
Unlike real FreeIPA/Dogtag, the stub does not enforce subject name patterns or key type constraints defined in the profile config files.
0 commit comments