Skip to content

Commit 0449a15

Browse files
committed
feat: add GET /profile/scep-challenge endpoint and SCEP platform research
The new endpoint issues a one-time challenge token (15-min TTL) for use with any SCEP client — enabling Windows enrollment via PSCertificateEnrollment and Linux enrollment via strongswan pki or sscep without downloading a mobileconfig. Also adds research/SCEP_PLATFORMS.md documenting enrollment flows for Windows and Linux, automatic renewal approaches, and why Android remains on PKCS#12.
1 parent 756aaf6 commit 0449a15

3 files changed

Lines changed: 212 additions & 0 deletions

File tree

cmd/pint/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ func main() {
245245
protected.GET("/profile", handlers.ProfilePageHandler(cfg))
246246
protected.POST("/profile/generate", handlers.GenerateProfileHandler(log, ipaClient, cfg, caDER, rootCACertDER, codeSigningCACertDER, scepRACertDER, challengeStore, appleSigner))
247247
protected.GET("/profile/ca", handlers.CAHandler(caDER))
248+
protected.GET("/profile/scep-challenge", handlers.SCEPChallengeHandler(log, challengeStore))
248249
protected.GET("/radius", handlers.RadiusPageHandler(cfg, k8sClient, radSecCAChainPEM))
249250
protected.POST("/radius/secret", handlers.SaveSecretHandler(log, ipaClient, cfg, k8sClient, radSecCAChainPEM))
250251
protected.POST("/radius/regenerate", handlers.RegenerateHandler(log, ipaClient, cfg, k8sClient, radSecCAChainPEM))

internal/handlers/profile.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,26 @@ func GenerateProfileHandler(log *zap.Logger, ipaClient *freeipa.Client, cfg *con
122122
}
123123
}
124124

125+
// SCEPChallengeHandler serves GET /profile/scep-challenge.
126+
// Issues a one-time challenge token that can be used with any SCEP client
127+
// (e.g. Get-SCEPCertificate on Windows, sscep or strongswan pki on Linux).
128+
func SCEPChallengeHandler(log *zap.Logger, challenges *scep.ChallengeStore) gin.HandlerFunc {
129+
return func(c *gin.Context) {
130+
username, ok := getUsername(c)
131+
if !ok {
132+
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authenticated"})
133+
return
134+
}
135+
token, err := challenges.Issue(username)
136+
if err != nil {
137+
log.Error("challenge generation failed", zap.Error(err))
138+
c.JSON(http.StatusInternalServerError, gin.H{"error": "challenge generation failed"})
139+
return
140+
}
141+
c.JSON(http.StatusOK, gin.H{"challenge": token})
142+
}
143+
}
144+
125145
// CAHandler serves GET /profile/ca, returns the FreeIPA CA certificate as PEM.
126146
func CAHandler(caDER []byte) gin.HandlerFunc {
127147
caPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caDER})

research/SCEP_PLATFORMS.md

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
# SCEP Platform Research
2+
3+
PINT already implements SCEP end-to-end for iOS/macOS using Apple's native mobileconfig SCEP payload and automatic renewal. This document covers research into SCEP enrollment for other platforms and the PINT endpoints available for manual enrollment.
4+
5+
---
6+
7+
## PINT SCEP Endpoints
8+
9+
### `GET /scep` / `POST /scep`
10+
11+
The SCEP RA endpoint. Handles `GetCACaps`, `GetCACert`, and `PKIOperation`. No authentication required — the one-time challenge password in the enrollment request is the auth.
12+
13+
### `GET /profile/scep-challenge`
14+
15+
Requires an active PINT session. Issues a one-time challenge token (15-minute TTL) and returns it as JSON:
16+
17+
```json
18+
{"challenge": "a3f8b1c2d4e5f6a7b8c9d0e1f2a3b4c5"}
19+
```
20+
21+
Use this token as the challenge password in any SCEP client. A new token is required for each enrollment — tokens are consumed on use.
22+
23+
```bash
24+
# Fetch a token (substitute your session cookie)
25+
TOKEN=$(curl -s -b <session-cookie> https://pint.csh.rit.edu/profile/scep-challenge | jq -r .challenge)
26+
```
27+
28+
---
29+
30+
## Windows
31+
32+
### Current state
33+
34+
Windows has no built-in SCEP client that works against a generic SCEP server. The native tooling (`Get-Certificate`, `certmgr.msc`) only speaks Microsoft's own XCEP/WSTEP protocols. However, the PSCertificateEnrollment PowerShell module wraps Windows's `IX509SCEPEnrollment` COM interface and is documented as compatible with any RFC 8894 server. It is the most practical path today.
35+
36+
### Enrollment with PSCertificateEnrollment
37+
38+
```powershell
39+
# One-time: install the module
40+
Install-Module PSCertificateEnrollment
41+
42+
# Fetch a challenge token from PINT (requires browser session cookie)
43+
$token = (Invoke-RestMethod -Uri https://pint.csh.rit.edu/profile/scep-challenge `
44+
-Headers @{ Cookie = "<session-cookie>" }).challenge
45+
46+
# Enroll
47+
Get-SCEPCertificate `
48+
-SCEPServerURL https://pint.csh.rit.edu/scep `
49+
-ChallengePassword $token `
50+
-Subject "CN=$env:USERNAME"
51+
```
52+
53+
The certificate is installed directly into the Windows certificate store (Personal). The WLAN XML profile (`/profile/generate?platform=windows`) can then reference it for EAP-TLS.
54+
55+
### Automatic renewal
56+
57+
Renewal is scriptable using the existing certificate as authentication:
58+
59+
```powershell
60+
# Renew using the expiring cert (no new challenge needed)
61+
Get-SCEPCertificate `
62+
-SCEPServerURL https://pint.csh.rit.edu/scep `
63+
-SigningCertificate (Get-ChildItem Cert:\CurrentUser\My | Where-Object { $_.Subject -like "CN=$env:USERNAME*" })
64+
```
65+
66+
Wrap this in a Task Scheduler job at ~50% of the 1-year cert lifetime (around 6 months) for fully automatic renewal.
67+
68+
### Future: MS-XCEP + MS-WSTEP (native GUI enrollment)
69+
70+
The only path to a true `certmgr.msc` GUI experience on non-domain Windows machines without MDM. The flow:
71+
72+
1. User adds the PINT XCEP URL to `certmgr.msc` under Manage Enrollment Policies (one-time setup).
73+
2. Right-click Personal > Request New Certificate > select the PINT template.
74+
3. Certificate is issued and installed.
75+
76+
Authentication would use FreeIPA username/password via WS-Security UsernameToken — no Active Directory required. The specs (MS-XCEP and MS-WSTEP) are fully published open specifications. No production Go library exists for this; it would need to be implemented from scratch. Estimated effort: 1-3 weeks. Renewal from `certmgr.msc` would also work (right-click the cert > Renew), but automatic background renewal would require additionally implementing MS-CEAS (autoenrollment trigger).
77+
78+
---
79+
80+
## Linux
81+
82+
### Current state
83+
84+
No distro ships a SCEP client by default, but two well-maintained options cover the use case well. Both are packaged for Debian/Ubuntu.
85+
86+
### Option 1: strongSwan `pki --scep` (recommended)
87+
88+
Implements RFC 8894 (the current standard). Ships AES + SHA-256 by default. Better long-term interoperability with PINT's modern SCEP server.
89+
90+
```bash
91+
# Install
92+
apt install strongswan-pki
93+
94+
# Fetch a challenge token
95+
TOKEN=$(curl -s -b <session-cookie> https://pint.csh.rit.edu/profile/scep-challenge | jq -r .challenge)
96+
97+
# Generate a private key
98+
pki --gen --type rsa --size 2048 --outform pem > wifi-client.key
99+
100+
# Enroll
101+
pki --scep \
102+
--url https://pint.csh.rit.edu/scep \
103+
--in wifi-client.key \
104+
--dn "CN=$(whoami),O=CSH.RIT.EDU" \
105+
--password "$TOKEN" \
106+
> wifi-client.crt
107+
108+
# Configure wpa_supplicant or NetworkManager to use wifi-client.key + wifi-client.crt
109+
```
110+
111+
Renewal uses the existing cert to authenticate (no new challenge required):
112+
113+
```bash
114+
pki --scep \
115+
--url https://pint.csh.rit.edu/scep \
116+
--in wifi-client.key \
117+
--cert wifi-client.crt \
118+
--dn "CN=$(whoami),O=CSH.RIT.EDU" \
119+
> wifi-client.crt.new && mv wifi-client.crt.new wifi-client.crt
120+
```
121+
122+
### Option 2: sscep
123+
124+
Implements an older SCEP draft but works against most servers. Debian-packaged (`apt install sscep`). Useful if strongSwan is not available.
125+
126+
```bash
127+
apt install sscep openssl
128+
129+
TOKEN=$(curl -s -b <session-cookie> https://pint.csh.rit.edu/profile/scep-challenge | jq -r .challenge)
130+
131+
# Generate key and CSR
132+
openssl genrsa -out wifi-client.key 2048
133+
openssl req -new -key wifi-client.key -out wifi-client.csr \
134+
-subj "/CN=$(whoami)/O=CSH.RIT.EDU"
135+
136+
# Fetch the SCEP CA cert
137+
sscep getca -u https://pint.csh.rit.edu/scep -c scep-ca.crt
138+
139+
# Enroll
140+
sscep enroll \
141+
-u https://pint.csh.rit.edu/scep \
142+
-c scep-ca.crt \
143+
-k wifi-client.key \
144+
-r wifi-client.csr \
145+
-l wifi-client.crt \
146+
-p "$TOKEN"
147+
```
148+
149+
Renewal with sscep (uses existing cert/key, no new challenge):
150+
151+
```bash
152+
sscep enroll \
153+
-u https://pint.csh.rit.edu/scep \
154+
-c scep-ca.crt \
155+
-k wifi-client.key \
156+
-r wifi-client.csr \
157+
-l wifi-client.crt \
158+
-K wifi-client.key \
159+
-O wifi-client.crt
160+
```
161+
162+
### Automatic renewal via systemd timer
163+
164+
```ini
165+
# /etc/systemd/system/pint-wifi-renew.service
166+
[Unit]
167+
Description=Renew PINT WiFi certificate
168+
169+
[Service]
170+
Type=oneshot
171+
ExecStart=/usr/local/bin/pint-wifi-renew.sh
172+
173+
# /etc/systemd/system/pint-wifi-renew.timer
174+
[Unit]
175+
Description=PINT WiFi certificate renewal check
176+
177+
[Timer]
178+
OnCalendar=monthly
179+
Persistent=true
180+
181+
[Install]
182+
WantedBy=timers.target
183+
```
184+
185+
The renewal script checks remaining validity with `openssl x509 -checkend` and only re-enrolls if the cert is within 30 days of expiry.
186+
187+
---
188+
189+
## Android
190+
191+
No viable path for unmanaged personal devices. Android's certificate APIs only support SCEP enrollment through a full MDM/EMM enrollment (Intune, Workspace ONE, etc.) — there is no user-initiated flow for installing a SCEP-enrolled certificate into the system keystore for Wi-Fi use without MDM. The current PKCS#12 download flow is the best available option for Android.

0 commit comments

Comments
 (0)