docs: convert all 9 ASCII diagrams to mermaid

Audit of docs/ found 32 diagrams: 23 already in mermaid, 9 in ASCII
art (box-drawing chars / +-pipe boxes). Converting all 9 to mermaid
so GitHub renders them as actual diagrams in the docs preview.

Files affected (9 diagram blocks across 6 files):

  docs/architecture.md   block 1 line 706  EST request flow
  docs/architecture.md   block 2 line 798  SCEP request flow
  docs/architecture.md   block 3 line 893  Per-profile TrustAnchor +
                                           Intune challenge dispatch
  docs/architecture.md   block 4 line 935  signer.Driver interface +
                                           4 implementations
  docs/ci-pipeline.md    block 1 line 20   On-push pipeline tree
  docs/est.md            block 1 line 254  WiFi 802.1X / EAP-TLS flow
  docs/legacy-est-scep.md block 1 line 40  TLS-version-bridging proxy
  docs/qa-test-guide.md  block 1 line 41   qa_test.go to demo stack
  docs/scep-intune.md    block 1 line 39   Intune cloud chain

Conversion notes:

  - Linear flows → flowchart TD/LR. Per-step annotations that the
    ASCII had as floating text between arrows are now edge labels —
    cleaner and easier to read.
  - architecture.md block 4 (signer drivers) → flowchart LR with a
    subgraph for the Driver interface. Cleaner than a class diagram
    for the "code uses one of these implementations" semantics.
  - ci-pipeline.md tree → flowchart TD. Adds a dotted '-.depends
    on.->' arrow making the go-build-and-test → deploy-vendor-e2e
    dependency visually obvious (was a parenthetical in the ASCII).
  - est.md WiFi/RADIUS → flowchart LR with EAP, Radius, trusts,
    and EST as four distinct labeled arrows. The 'trusts' annotation
    was floating off to the side in the ASCII; now it's the arrow
    label between Radius and certctl CA.
  - All semantic detail preserved: every node label, arrow direction,
    inline annotation, and multi-line cell content carries through.

Verified: post-conversion audit shows 32 mermaid blocks, 0 ASCII.
Diff is symmetric — 108 inserts, 123 deletes — because mermaid is
slightly more compact than the box-drawing characters it replaces.

GitHub renders mermaid blocks natively in markdown previews since
2022, so all 9 diagrams now render as real flowcharts in the docs
view rather than as monospaced character art.
This commit is contained in:
shankar0123
2026-05-01 05:09:00 +00:00
parent 2643a427ac
commit dcd82d062f
6 changed files with 108 additions and 123 deletions
+53 -61
View File
@@ -703,20 +703,17 @@ The EST (Enrollment over Secure Transport) server provides an industry-standard
**Architecture:** EST is a handler-level protocol that delegates certificate issuance to an existing `IssuerConnector`. This means EST is not a new issuer — it's a new *interface* to the existing issuance infrastructure. The `ESTService` bridges the `ESTHandler` to whichever issuer connector is configured via `CERTCTL_EST_ISSUER_ID`.
```
Client (WiFi AP, MDM, IoT)
ESTHandler (handler layer)
│ CSR parsing, PKCS#7 response encoding
ESTService (service layer)
│ CSR validation, CN/SAN extraction, audit recording
IssuerConnector (connector layer via IssuerConnectorAdapter)
│ Certificate signing (Local CA, step-ca, etc.)
Signed certificate returned as PKCS#7 certs-only
```mermaid
flowchart TD
Client["Client (WiFi AP, MDM, IoT)"]
Handler["ESTHandler (handler layer)"]
Service["ESTService (service layer)"]
Issuer["IssuerConnector (connector layer via IssuerConnectorAdapter)"]
Result["Signed certificate returned as PKCS#7 certs-only"]
Client --> Handler
Handler -->|"CSR parsing, PKCS#7 response encoding"| Service
Service -->|"CSR validation, CN/SAN extraction, audit recording"| Issuer
Issuer -->|"certificate signing (Local CA, step-ca, etc.)"| Result
```
**Wire format:** EST uses PKCS#7 (RFC 2315) certs-only degenerate SignedData for certificate responses and base64-encoded DER for CSR requests. The handler includes a hand-rolled ASN.1 PKCS#7 builder — no external PKCS#7 dependency. The CSR reader accepts both base64-encoded DER (standard EST wire format) and PEM-encoded PKCS#10 (convenience for debugging).
@@ -795,20 +792,17 @@ The SCEP (Simple Certificate Enrollment Protocol) server provides certificate en
**Architecture:** SCEP follows the exact same layering as EST — a handler-level protocol that delegates certificate issuance to an existing `IssuerConnector`. The `SCEPService` bridges the `SCEPHandler` to whichever issuer connector is configured via `CERTCTL_SCEP_ISSUER_ID`.
```
Client (MDM, network device, SCEP client)
SCEPHandler (handler layer)
│ PKCS#7 envelope parsing, CSR extraction, challenge password extraction
SCEPService (service layer)
│ Challenge password validation, CSR validation, CN/SAN extraction, audit recording
IssuerConnector (connector layer via IssuerConnectorAdapter)
│ Certificate signing (Local CA, step-ca, etc.)
Signed certificate returned as PKCS#7 certs-only
```mermaid
flowchart TD
Client["Client (MDM, network device, SCEP client)"]
Handler["SCEPHandler (handler layer)"]
Service["SCEPService (service layer)"]
Issuer["IssuerConnector (connector layer via IssuerConnectorAdapter)"]
Result["Signed certificate returned as PKCS#7 certs-only"]
Client --> Handler
Handler -->|"PKCS#7 envelope parsing, CSR extraction, challenge password extraction"| Service
Service -->|"challenge password validation, CSR validation, CN/SAN extraction, audit recording"| Issuer
Issuer -->|"certificate signing (Local CA, step-ca, etc.)"| Result
```
**Wire format:** Two paths, tried in order. The new RFC 8894 path (post-2026-04-29) parses the full PKIMessage shape: ContentInfo → SignedData → SignerInfo (POPO over auth-attrs verified via `internal/pkcs7/signedinfo.go::SignerInfo.VerifySignature` with the canonical SET-OF Attribute re-serialisation per RFC 5652 §5.4) → EnvelopedData (decrypted via `internal/pkcs7/envelopeddata.go::EnvelopedData.Decrypt` with RSA PKCS#1v1.5 keyTrans + AES-CBC content + constant-time PKCS#7 unpad to close the padding-oracle leak) → inner PKCS#10 CSR. Auth-attrs (messageType, transactionID, senderNonce) flow through to the service layer via `domain.SCEPRequestEnvelope`. The handler dispatches on messageType: PKCSReq (19) → initial enrollment; RenewalReq (17) → re-enrollment with chain validation; GetCertInitial (20) → polling stub returns FAILURE+badCertID. Responses are full CertRep PKIMessages (`internal/pkcs7/certrep.go::BuildCertRepPKIMessage`) signed by the per-profile RA cert/key with the issued cert chain encrypted to the device's transient signing cert (RFC 8894 §3.3.2). On parse failure the handler falls through to the legacy MVP path: base64-encoded PKCS#7 and raw CSR submissions are still accepted; responses use the legacy PKCS#7 certs-only shape via the shared `internal/pkcs7` package. The MVP fall-through is non-negotiable — backward compat with lightweight SCEP clients that don't speak full RFC 8894. Single certs are returned as raw DER for `GetCACert`, chains as PKCS#7.
@@ -890,23 +884,27 @@ each per-profile dispatcher carries its own **trust anchor pool**:
the public certs the operator extracted from the Connector's
installation. Every Intune-flavored enrollment goes through:
```
┌─────────────────────────────────┐
Per-profile TrustAnchorHolder
│ (RWMutex pool, SIGHUP-reloadable) │
└────────────┬────────────────────┘
│ Get()
device → SCEP PKIMessage → handler → SCEPService.dispatchIntuneChallenge
├─► intune.ValidateChallenge (sig + iat/exp + audience)
├─► claim.DeviceMatchesCSR (set-equality)
├─► intune.ReplayCache.CheckAndInsert
├─► intune.PerDeviceRateLimiter.Allow
└─► (V3-Pro) ComplianceCheck hook
processEnrollment → IssuerConnector
```mermaid
flowchart TD
TAH["Per-profile TrustAnchorHolder<br/>(RWMutex pool, SIGHUP-reloadable)"]
Device[device]
Handler[handler]
Dispatch["SCEPService.dispatchIntuneChallenge"]
Validate["intune.ValidateChallenge<br/>(sig + iat/exp + audience)"]
Match["claim.DeviceMatchesCSR<br/>(set-equality)"]
Replay["intune.ReplayCache.CheckAndInsert"]
Rate["intune.PerDeviceRateLimiter.Allow"]
Compliance["(V3-Pro) ComplianceCheck hook"]
Process["processEnrollment → IssuerConnector"]
Device -->|SCEP PKIMessage| Handler
Handler --> Dispatch
TAH -.->|Get()| Dispatch
Dispatch --> Validate
Dispatch --> Match
Dispatch --> Replay
Dispatch --> Rate
Dispatch --> Compliance
Dispatch --> Process
```
The trust anchor file is mode-0600 on disk; certctl loads it at
@@ -932,22 +930,16 @@ See [`scep-intune.md`](scep-intune.md) for the full migration playbook
The local issuer's CA private key is wrapped behind the `signer.Signer` interface in `internal/crypto/signer/`. Every CA-signing call site — leaf certificate issuance (`x509.CreateCertificate`), CRL generation (`x509.CreateRevocationList`), and OCSP response signing (`ocsp.CreateResponse`) — accesses the key through this interface rather than touching `crypto.Signer` directly. The interface embeds the stdlib `crypto.Signer` and adds a single `Algorithm() Algorithm` method so call sites can pick the matching `x509.SignatureAlgorithm` without reflecting on the concrete key type.
```
┌─────────────────────────────────┐
│ signer.Driver (pluggable) │
├─────────────────────────────────┤
internal/connector/issuer/local signer.FileDriver (default)
c.caSigner signer.Signer ──────────► │ PEM key on disk │
│ │
│ signer.MemoryDriver (tests) │
│ in-memory only │
│ │
│ signer.PKCS11Driver (V3-Pro) │
│ HSM token (future) │
│ │
│ signer.CloudKMSDriver (V3-Pro) │
│ AWS / GCP / Azure (future) │
└─────────────────────────────────┘
```mermaid
flowchart LR
Local["internal/connector/issuer/local<br/>c.caSigner signer.Signer"]
subgraph Driver["signer.Driver (pluggable)"]
File["signer.FileDriver (default)<br/>PEM key on disk"]
Memory["signer.MemoryDriver (tests)<br/>in-memory only"]
PKCS11["signer.PKCS11Driver (V3-Pro)<br/>HSM token (future)"]
Cloud["signer.CloudKMSDriver (V3-Pro)<br/>AWS / GCP / Azure (future)"]
end
Local --> Driver
```
Today only `FileDriver` (production) and `MemoryDriver` (tests) ship. The interface exists so PKCS#11/HSM and cloud-KMS drivers can land in follow-on packages (`internal/crypto/signer/pkcs11`, etc.) without modifying any call site or any other driver. The L-014 file-on-disk threat-model carve-out documented at the top of `internal/connector/issuer/local/local.go` applies to `FileDriver`-backed signers; alternative drivers that keep the key inside an HSM token or cloud KMS close the disk-exposure leg of the threat model entirely.
+22 -11
View File
@@ -17,17 +17,28 @@ This guide covers the **on-push pipeline** only.
## On-push pipeline (7 status checks)
```
push to master
├── CI workflow (5 jobs)
│ ├── go-build-and-test (~6-7 min)
│ ├── frontend-build (~1 min)
│ ├── helm-lint (~10 sec)
│ ├── deploy-vendor-e2e (~5 min, depends on go-build-and-test)
│ └── image-and-supply-chain (~3 min, parallel)
└── CodeQL workflow (2 jobs)
├── Analyze (go) (~5 min, parallel)
└── Analyze (javascript-typescript) (~5 min, parallel)
```mermaid
flowchart TD
Push["push to master"]
CI["CI workflow (5 jobs)"]
CodeQL["CodeQL workflow (2 jobs)"]
GoBuild["go-build-and-test<br/>~6-7 min"]
Frontend["frontend-build<br/>~1 min"]
HelmLint["helm-lint<br/>~10 sec"]
Vendor["deploy-vendor-e2e<br/>~5 min, depends on go-build-and-test"]
Image["image-and-supply-chain<br/>~3 min, parallel"]
AnalyzeGo["Analyze (go)<br/>~5 min, parallel"]
AnalyzeJS["Analyze (javascript-typescript)<br/>~5 min, parallel"]
Push --> CI
Push --> CodeQL
CI --> GoBuild
CI --> Frontend
CI --> HelmLint
CI --> Vendor
CI --> Image
CodeQL --> AnalyzeGo
CodeQL --> AnalyzeJS
GoBuild -.depends on.-> Vendor
```
End-to-end wall-clock: dominated by `go-build-and-test` + `deploy-vendor-e2e` chain (~12 min) running in parallel with CodeQL (~5 min). Target ~10 min.
+10 -14
View File
@@ -251,20 +251,16 @@ This recipe stands up an EAP-TLS-authenticated corporate WiFi network
where certctl issues every device certificate via EST. End-to-end
flow:
```
┌─────────────┐ ┌──────────────────┐ ┌─────────────┐
│ Laptop / │ EAP │ WiFi access │ Radius│ FreeRADIUS │
│ supplicant │─────▶│ point (NAS) │──────▶│ (validate │
│ (wpa_ │ │ │ │ cert chain)
│ supplicant │ └──────────────────┘ └──────┬──────┘
│ / iwd / │ │
│ Apple WiFi)│ │ trusts
└──────┬──────┘ ▼
│ EST (one-time, then renewal) ┌─────────────┐
│ /simpleenroll, /simplereenroll │ certctl CA │
└────────────────────────────────────▶│ (EST profile│
│ "wifi") │
└─────────────┘
```mermaid
flowchart LR
Laptop["Laptop / supplicant<br/>(wpa_supplicant / iwd / Apple WiFi)"]
AP["WiFi access point (NAS)"]
Radius["FreeRADIUS<br/>(validate cert chain)"]
CA["certctl CA<br/>(EST profile 'wifi')"]
Laptop -->|EAP| AP
AP -->|Radius| Radius
Radius -.->|trusts| CA
Laptop -->|"EST: /simpleenroll, /simplereenroll<br/>(one-time, then renewal)"| CA
```
### certctl-side: EST profile config for 802.1X
+7 -6
View File
@@ -37,12 +37,13 @@ straight at certctl on `:8443`.
## Architecture
```
┌─── TLS 1.2/1.3 ────┐ ┌─── TLS 1.3 ───┐
[legacy EST/SCEP client]──>│ nginx / HAProxy │────────>│ certctl :8443 │
│ reverse proxy │ │ │
└────────────────────┘ └───────────────┘
Allowed TLS 1.2 Re-encrypts as TLS 1.3
```mermaid
flowchart LR
Client["legacy EST/SCEP client"]
Proxy["nginx / HAProxy<br/>reverse proxy"]
Server["certctl :8443"]
Client -->|"TLS 1.2/1.3<br/>(allowed TLS 1.2)"| Proxy
Proxy -->|"TLS 1.3<br/>(re-encrypts as TLS 1.3)"| Server
```
The reverse proxy:
+9 -16
View File
@@ -38,22 +38,15 @@ either manual-only by design or pending QA-suite coverage:
## Architecture
```
┌────────────────────────┐ ┌─────────────────────────────────┐
qa_test.go │────▶│ certctl demo stack │
│ (//go:build qa) │ │ docker-compose.yml + │
│ │ │ docker-compose.demo.yml │
│ TestQA(t *testing.T) │ │ │
│ ├─ Part01_Infra │ │ ┌─ certctl-server :8443 │
│ ├─ Part02_Auth │ │ ├─ postgres :5432 │
│ ├─ Part03_CertCRUD │ │ └─ certctl-agent (×N) │
│ ├─ ... │ │ ↑ seed_demo.sql provisions │
│ └─ Part52_HelmChart │ │ 12 agent rows (1 active, │
└────────────────────────┘ │ 2 retired, 9 reserved / │
│ sentinel) for the soft- │
│ retire / FSM coverage │
│ Parts 5556 exercise. │
└─────────────────────────────────┘
```mermaid
flowchart LR
QA["qa_test.go (//go:build qa)<br/><br/>TestQA(t *testing.T)<br/>├─ Part01_Infra<br/>├─ Part02_Auth<br/>├─ Part03_CertCRUD<br/>├─ ...<br/>└─ Part52_HelmChart"]
subgraph Stack["certctl demo stack<br/>docker-compose.yml + docker-compose.demo.yml"]
Server["certctl-server :8443"]
Postgres["postgres :5432"]
Agents["certctl-agent (×N)<br/>↑ seed_demo.sql provisions 12 agent rows<br/>(1 active, 2 retired, 9 reserved/sentinel)<br/>for the soft-retire / FSM coverage Parts 5556 exercise"]
end
QA --> Stack
```
> **Multi-agent demo stack (Bundle Q / L-004 closure).** The demo
+7 -15
View File
@@ -36,21 +36,13 @@ What you get over NDES:
## Architecture
```
┌──────────────┐ ┌──────────────────────┐ ┌──────────────┐
│ Intune cloud │──────▶│ Intune Certificate │──────▶│ certctl SCEP │
│ │ │ Connector │ │ server │
│ (Microsoft) │ │ (customer infra) │ │ (you) │
└──────────────┘ └──────────────────────┘ └──────┬───────┘
┌──────────────┐
│ issuer │
│ connector │
│ (local CA / │
│ Vault / │
│ EJBCA / …) │
└──────────────┘
```mermaid
flowchart LR
Cloud["Intune cloud<br/>(Microsoft)"]
Connector["Intune Certificate Connector<br/>(customer infra)"]
Server["certctl SCEP server<br/>(you)"]
Issuer["issuer connector<br/>(local CA / Vault / EJBCA / …)"]
Cloud --> Connector --> Server --> Issuer
```
**certctl replaces NDES, not the Connector.** The Intune Certificate