feat: add EST server (RFC 7030) for device certificate enrollment (M23)

Implement Enrollment over Secure Transport protocol with 4 endpoints under
/.well-known/est/ — cacerts (CA chain distribution), simpleenroll (initial
enrollment), simplereenroll (certificate renewal), and csrattrs (CSR
attributes). PKCS#7 certs-only wire format with hand-rolled ASN.1, accepts
both PEM and base64-encoded DER CSRs, configurable issuer and profile
binding, full audit trail. 28 new tests (18 handler + 10 service).

Also includes:
- GetCACertPEM added to issuer connector interface (all 4 issuers updated)
- EST integration tests wired into e2e test suite (13 test cases)
- QA testing guide Part 26 (15 manual EST test cases)
- All docs updated: README, features, architecture, concepts, connectors,
  quickstart, demo-advanced (endpoint counts, MCP wording, agent IDs,
  issuer interface, resource lists, OpenSSL status)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Shankar
2026-03-25 15:31:06 -04:00
parent 42fa9c7791
commit e4ba8d4de2
27 changed files with 1807 additions and 20 deletions
+241 -1
View File
@@ -3832,9 +3832,248 @@ grep -rn "errors.Is.*errors.New\|errors.Is(.*err.*errors.New" internal/service/*
---
## Part 26: EST Server (RFC 7030)
**Scope:** Enrollment over Secure Transport — 4 endpoints under `/.well-known/est/` for device certificate enrollment. Tests cover CA cert distribution, certificate enrollment (PEM and base64-DER CSR formats), re-enrollment, CSR attributes, wire format compliance, and error handling.
**Prerequisites:** Server running with `CERTCTL_EST_ENABLED=true`, `CERTCTL_EST_ISSUER_ID=iss-local` (or a valid issuer). An ECDSA P-256 key pair and CSR for enrollment tests.
---
**Test 26.1 — GET /.well-known/est/cacerts returns PKCS#7 CA chain**
```bash
curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $API_KEY" \
http://localhost:8443/.well-known/est/cacerts
```
**Expected:** HTTP 200, `Content-Type: application/pkcs7-mime`, `Content-Transfer-Encoding: base64`. Body is base64-encoded degenerate PKCS#7 SignedData containing the CA certificate chain.
**PASS if** status = 200, correct content type, non-empty body.
---
**Test 26.2 — GET /cacerts method enforcement**
```bash
curl -s -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: Bearer $API_KEY" \
http://localhost:8443/.well-known/est/cacerts
```
**Expected:** HTTP 405 Method Not Allowed.
**PASS if** status = 405.
---
**Test 26.3 — POST /.well-known/est/simpleenroll with PEM CSR**
Generate a test CSR and submit as PEM:
```bash
# Generate ECDSA P-256 key and CSR
openssl ecparam -name prime256v1 -genkey -noout -out /tmp/est-test.key
openssl req -new -key /tmp/est-test.key -out /tmp/est-test.csr \
-subj "/CN=est-test.example.com" \
-addext "subjectAltName=DNS:est-test.example.com"
# Submit PEM CSR
curl -s -w "\n%{http_code}" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/pkcs10" \
--data-binary @/tmp/est-test.csr \
http://localhost:8443/.well-known/est/simpleenroll
```
**Expected:** HTTP 200, `Content-Type: application/pkcs7-mime`, `Content-Transfer-Encoding: base64`. Body contains base64-encoded PKCS#7 with the signed certificate.
**PASS if** status = 200, response decodes to valid PKCS#7.
---
**Test 26.4 — POST /simpleenroll with base64-encoded DER CSR**
```bash
# Convert PEM CSR to base64-encoded DER (EST wire format)
openssl req -in /tmp/est-test.csr -outform DER | base64 > /tmp/est-test-b64der.csr
curl -s -w "\n%{http_code}" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/pkcs10" \
--data-binary @/tmp/est-test-b64der.csr \
http://localhost:8443/.well-known/est/simpleenroll
```
**Expected:** HTTP 200. Server auto-detects base64-encoded DER and converts to PEM internally.
**PASS if** status = 200.
---
**Test 26.5 — POST /simpleenroll with empty body**
```bash
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/pkcs10" \
-X POST -d "" \
http://localhost:8443/.well-known/est/simpleenroll
```
**Expected:** HTTP 400 Bad Request.
**PASS if** status = 400.
---
**Test 26.6 — POST /simpleenroll with invalid CSR**
```bash
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/pkcs10" \
-X POST -d "not-a-valid-csr-at-all" \
http://localhost:8443/.well-known/est/simpleenroll
```
**Expected:** HTTP 400 Bad Request.
**PASS if** status = 400.
---
**Test 26.7 — POST /simpleenroll with CSR missing Common Name**
```bash
openssl ecparam -name prime256v1 -genkey -noout -out /tmp/est-nocn.key
openssl req -new -key /tmp/est-nocn.key -out /tmp/est-nocn.csr -subj "/"
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/pkcs10" \
--data-binary @/tmp/est-nocn.csr \
http://localhost:8443/.well-known/est/simpleenroll
```
**Expected:** HTTP 500 (service returns error for missing CN). Error message should reference "Common Name".
**PASS if** status != 200.
---
**Test 26.8 — POST /simpleenroll method enforcement (GET not allowed)**
```bash
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $API_KEY" \
http://localhost:8443/.well-known/est/simpleenroll
```
**Expected:** HTTP 405 Method Not Allowed.
**PASS if** status = 405.
---
**Test 26.9 — POST /.well-known/est/simplereenroll (re-enrollment)**
```bash
openssl ecparam -name prime256v1 -genkey -noout -out /tmp/est-renew.key
openssl req -new -key /tmp/est-renew.key -out /tmp/est-renew.csr \
-subj "/CN=renew-est.example.com" \
-addext "subjectAltName=DNS:renew-est.example.com"
curl -s -w "\n%{http_code}" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/pkcs10" \
--data-binary @/tmp/est-renew.csr \
http://localhost:8443/.well-known/est/simplereenroll
```
**Expected:** HTTP 200. Functionally identical to simpleenroll per RFC 7030 Section 4.2.2.
**PASS if** status = 200, valid PKCS#7 response.
---
**Test 26.10 — GET /simplereenroll method enforcement**
```bash
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $API_KEY" \
http://localhost:8443/.well-known/est/simplereenroll
```
**Expected:** HTTP 405 Method Not Allowed.
**PASS if** status = 405.
---
**Test 26.11 — GET /.well-known/est/csrattrs returns 204 (no required attrs)**
```bash
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $API_KEY" \
http://localhost:8443/.well-known/est/csrattrs
```
**Expected:** HTTP 204 No Content (default implementation requires no specific CSR attributes).
**PASS if** status = 204.
---
**Test 26.12 — POST /csrattrs method enforcement**
```bash
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $API_KEY" \
-X POST http://localhost:8443/.well-known/est/csrattrs
```
**Expected:** HTTP 405 Method Not Allowed.
**PASS if** status = 405.
---
**Test 26.13 — EST enrollment creates audit event**
After a successful simpleenroll request (Test 26.3), query the audit trail:
```bash
curl -s -H "Authorization: Bearer $API_KEY" \
"http://localhost:8443/api/v1/audit?page=1&per_page=10" | \
jq '.data[] | select(.action == "est_simple_enroll")'
```
**Expected:** At least one audit event with `action: "est_simple_enroll"`, `protocol: "EST"` in details, and the enrolled CN in the details.
**PASS if** audit event found with correct action and details.
---
**Test 26.14 — EST disabled returns 404**
With `CERTCTL_EST_ENABLED=false` (default), EST endpoints should not be registered:
```bash
curl -s -o /dev/null -w "%{http_code}" http://localhost:8443/.well-known/est/cacerts
```
**Expected:** HTTP 404 Not Found (endpoints not registered when EST is disabled).
**PASS if** status = 404.
---
**Test 26.15 — EST with profile binding**
With `CERTCTL_EST_PROFILE_ID=profile-wifi-client`, verify that audit events include the profile_id in their details:
```bash
# After enrollment with profile binding, check audit
curl -s -H "Authorization: Bearer $API_KEY" \
"http://localhost:8443/api/v1/audit?page=1&per_page=5" | \
jq '.data[0].details.profile_id'
```
**Expected:** Profile ID appears in audit event details when configured.
**PASS if** `profile_id` present in audit details.
---
## Release Sign-Off
All 25 parts must pass before tagging v2.0.0.
All 26 parts must pass before tagging v2.0.1.
| Section | Pass? | Tester | Date | Notes |
|---------|-------|--------|------|-------|
@@ -3863,6 +4102,7 @@ All 25 parts must pass before tagging v2.0.0.
| Part 23: Structured Logging | ☐ | | | |
| Part 24: Documentation Verification | ☐ | | | |
| Part 25: Regression Tests | ☐ | | | |
| Part 26: EST Server (RFC 7030) | ☐ | | | |
**Automated tests (900+) must also be green.** CI passing is necessary but not sufficient — this manual QA catches integration issues that isolated unit tests miss.