From 0594631e6a6e79fb1211b205d5d3ae29c6fde4ca Mon Sep 17 00:00:00 2001 From: shankar0123 Date: Wed, 29 Apr 2026 02:58:39 +0000 Subject: [PATCH] =?UTF-8?q?gui/cert-detail:=20revocation=20endpoints=20pan?= =?UTF-8?q?el=20(CRL/OCSP)=20=E2=80=94=20Phase=205?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CertificateDetailPage now surfaces a Revocation Endpoints card showing the standards-compliant /.well-known/pki/crl/{issuer_id} CRL distribution point (RFC 5280 §4.2.1.13) and /.well-known/pki/ocsp/{issuer_id} OCSP responder URL (RFC 6960 §A.1) for relying parties that don't already know certctl's well-known scheme. Two action buttons exercise the same network path the issued leaves' AIA/CDP extensions advertise, so an operator can confirm 'did the backend Phases 1-4 actually wire end-to-end?' without curl: * 'Test CRL fetch' — fetchCRL(issuer_id) helper, surfaces byte count * 'Check OCSP status' — getOCSPStatus(issuer_id, serial_hex) helper Admin-only cache-age badge: when useAuth().admin is true the panel pulls GET /api/v1/admin/crl/cache (M-008 admin-gated handler) and shows 'Cache fresh · 2m ago' / 'Cache stale' / 'Not yet generated' next to the heading. Non-admin callers don't trigger the fetch (gated client-side on enabled flag, server-side on middleware.IsAdmin) so the badge cannot leak generation cadence. Test coverage in CertificateDetailPage.test.tsx pins: 1. CRL + OCSP URLs render with issuer_id substituted 2. Test CRL fetch button calls fetchCRL with the issuer_id and renders the byte-count success message 3. Check OCSP status button calls getOCSPStatus with (issuer_id, serial) and renders the DER byte-count 4. Admin badge stays HIDDEN (and getAdminCRLCache is NEVER called) when useAuth().admin is false — pins the no-info-leak invariant P-1 closure docblock + CI guardrail (.github/workflows/ci.yml) updated to remove getOCSPStatus from the documented-orphan list since it now has a real consumer. types.ts: CRLCacheRow / CRLCacheEvent / CRLCacheResponse mirrors of the backend admin handler payload (admin_crl_cache.go). client.ts: fetchCRL + getAdminCRLCache helpers; getOCSPStatus already existed and is now an active consumer. Tests: 6/6 in CertificateDetailPage.test.tsx, 150/150 across api+page suite. tsc --noEmit clean. --- .github/workflows/ci.yml | 6 +- web/src/api/client.ts | 32 +++- web/src/api/types.ts | 40 +++++ web/src/pages/CertificateDetailPage.test.tsx | 142 ++++++++++++++++ web/src/pages/CertificateDetailPage.tsx | 165 ++++++++++++++++++- 5 files changed, 380 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae6f656..78f107a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1037,7 +1037,11 @@ jobs: # diff-04x03-d24864996ad4 + cat-b-dc46aadab98e for closure rationale. run: | set -e - DOCUMENTED='getAgentGroup getAgentGroupMembers getAuditEvent getCertificateDeployments getDiscoveredCertificate getHealthCheck getHealthCheckHistory getNetworkScanTarget getNotification getOCSPStatus getOwner getPolicy getPolicyViolations getRenewalPolicy getTeam registerAgent updateHealthCheck' + # CRL/OCSP-Responder Phase 5 closed the getOCSPStatus orphan: the + # CertificateDetailPage Revocation Endpoints panel now consumes it + # ("Check OCSP status" button). Removed from the list to keep the + # docblock + guardrail honest. + DOCUMENTED='getAgentGroup getAgentGroupMembers getAuditEvent getCertificateDeployments getDiscoveredCertificate getHealthCheck getHealthCheckHistory getNetworkScanTarget getNotification getOwner getPolicy getPolicyViolations getRenewalPolicy getTeam registerAgent updateHealthCheck' MISSING="" for fn in $DOCUMENTED; do if ! grep -qE "^export const ${fn}\b" web/src/api/client.ts; then diff --git a/web/src/api/client.ts b/web/src/api/client.ts index 305cb9a..f8b8f2c 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -1,4 +1,4 @@ -import type { Certificate, CertificateVersion, Agent, Job, Notification, AuditEvent, PolicyRule, PolicyViolation, RenewalPolicy, Issuer, Target, CertificateProfile, Owner, Team, AgentGroup, PaginatedResponse, DashboardSummary, CertificateStatusCount, ExpirationBucket, JobTrendDataPoint, IssuanceRateDataPoint, MetricsResponse, DiscoveredCertificate, DiscoveryScan, DiscoverySummary, NetworkScanTarget, EndpointHealthCheck, HealthHistoryEntry, HealthCheckSummary, AgentDependencyCounts, RetireAgentResponse, BlockedByDependenciesResponse } from './types'; +import type { Certificate, CertificateVersion, Agent, Job, Notification, AuditEvent, PolicyRule, PolicyViolation, RenewalPolicy, Issuer, Target, CertificateProfile, Owner, Team, AgentGroup, PaginatedResponse, DashboardSummary, CertificateStatusCount, ExpirationBucket, JobTrendDataPoint, IssuanceRateDataPoint, MetricsResponse, DiscoveredCertificate, DiscoveryScan, DiscoverySummary, NetworkScanTarget, EndpointHealthCheck, HealthHistoryEntry, HealthCheckSummary, AgentDependencyCounts, RetireAgentResponse, BlockedByDependenciesResponse, CRLCacheResponse } from './types'; const BASE = '/api/v1'; @@ -16,11 +16,16 @@ const BASE = '/api/v1'; // getAgentGroup, getAgentGroupMembers, getAuditEvent, // getCertificateDeployments, getDiscoveredCertificate, // getHealthCheck, getHealthCheckHistory, getNetworkScanTarget, -// getNotification, getOCSPStatus, getOwner, getPolicy, +// getNotification, getOwner, getPolicy, // getPolicyViolations, getRenewalPolicy, getTeam, registerAgent // (by-design pull-only; see C-1 closure docblock above its export), // updateHealthCheck. // +// CRL/OCSP-Responder Phase 5 closed the getOCSPStatus orphan: the +// CertificateDetailPage Revocation Endpoints panel now exercises it +// via the "Check OCSP status" button, so it's removed from the list +// above (and from the CI guardrail's DOCUMENTED list). +// // CI guardrail at .github/workflows/ci.yml::"Documented orphan // client fns sync guard (P-1)" enforces the docblock list ↔ // export list relationship: every name above must still be @@ -268,6 +273,29 @@ export const getOCSPStatus = (issuerId: string, serial: string) => { }); }; +// CRL/OCSP-Responder Phase 5: GUI-side helper for the "Test CRL fetch" button +// on CertificateDetailPage. Fetches the DER-encoded CRL from the well-known +// endpoint and returns the byte length so the panel can show "OK — N bytes". +// The Authorization header is intentionally omitted: /.well-known/pki/crl/ is +// the standards-compliant relying-party surface and runs unauthenticated. +export const fetchCRL = (issuerId: string) => { + return fetch(`/.well-known/pki/crl/${issuerId}`) + .then(async r => { + if (!r.ok) throw new Error(`CRL fetch failed: ${r.status}`); + const buf = await r.arrayBuffer(); + return { byteLength: buf.byteLength, contentType: r.headers.get('content-type') ?? '' }; + }); +}; + +// CRL/OCSP-Responder Phase 5 admin endpoint mirror. +// +// Backend handler: internal/api/handler/admin_crl_cache.go::ListCache. +// M-008 admin-gated; non-admin Bearer callers get HTTP 403 — the GUI hides +// the badge entirely (rather than letting it 403 noisily) by gating the +// React-Query enabled flag on useAuth().admin at the call site. +export const getAdminCRLCache = () => + fetchJSON(`${BASE}/admin/crl/cache`); + // Agents export const getAgents = (params: Record = {}) => { const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString(); diff --git a/web/src/api/types.ts b/web/src/api/types.ts index c5c065a..ca56b78 100644 --- a/web/src/api/types.ts +++ b/web/src/api/types.ts @@ -586,3 +586,43 @@ export interface HealthCheckSummary { unknown: number; total: number; } + +// CRL/OCSP-Responder Phase 5: admin observability endpoint payload mirror. +// +// Backend type lives at internal/api/handler/admin_crl_cache.go::CRLCacheRow / +// CRLCacheEvt and is gated behind middleware.IsAdmin (M-008 admin-gated handler +// allowlist). The GUI surfaces a per-issuer cache-age badge on the +// CertificateDetailPage Revocation Endpoints panel — only visible to admin +// callers. Non-admin callers get HTTP 403 from the server; the GUI suppresses +// the fetch entirely (and the badge) when useAuth().admin is false. +// +// Optional fields stay optional here because the server omits them when the +// cache row is absent (issuer never had a CRL generated yet) — the panel +// renders a "Not yet generated" pill in that case. +export interface CRLCacheEvent { + started_at: string; + duration_ms: number; + succeeded: boolean; + crl_number: number; + revoked_count: number; + error?: string; +} + +export interface CRLCacheRow { + issuer_id: string; + cache_present: boolean; + crl_number?: number; + this_update?: string; + next_update?: string; + generated_at?: string; + generation_duration_ms?: number; + revoked_count?: number; + is_stale?: boolean; + recent_events?: CRLCacheEvent[]; +} + +export interface CRLCacheResponse { + cache_rows: CRLCacheRow[]; + row_count: number; + generated_at: string; +} diff --git a/web/src/pages/CertificateDetailPage.test.tsx b/web/src/pages/CertificateDetailPage.test.tsx index 0d53a3f..0ececcd 100644 --- a/web/src/pages/CertificateDetailPage.test.tsx +++ b/web/src/pages/CertificateDetailPage.test.tsx @@ -30,6 +30,30 @@ vi.mock('../api/client', () => ({ updateCertificate: vi.fn(), downloadCertificatePEM: vi.fn(), exportCertificatePKCS12: vi.fn(), + // CRL/OCSP-Responder Phase 5: revocation-panel mocks. fetchCRL + + // getOCSPStatus are exercised by the "Test CRL fetch" / "Check OCSP + // status" buttons; getAdminCRLCache backs the admin cache-age badge + // and is gated by useAuth().admin at the call site. + getOCSPStatus: vi.fn(), + fetchCRL: vi.fn(), + getAdminCRLCache: vi.fn(), +})); + +// AuthProvider's useAuth hook is read by the new RevocationEndpointsCard to +// decide whether to render the cache-age badge. Mock it to keep the test +// independent of the real auth bootstrap (getAuthInfo / checkAuth). +vi.mock('../components/AuthProvider', () => ({ + useAuth: () => ({ + loading: false, + authRequired: false, + authenticated: true, + authType: 'none', + user: '', + admin: false, + login: vi.fn(), + logout: vi.fn(), + error: null, + }), })); import CertificateDetailPage from './CertificateDetailPage'; @@ -90,6 +114,12 @@ describe('CertificateDetailPage — render + XSS hardening (M-026 / M-029 Pass 3 vi.mocked(client.getProfiles).mockResolvedValue({ data: [], total: 0, page: 1, per_page: 50 } as never); vi.mocked(client.getRenewalPolicies).mockResolvedValue({ data: [], total: 0, page: 1, per_page: 500 } as never); vi.mocked(client.getJobs).mockResolvedValue({ data: [], total: 0, page: 1, per_page: 50 } as never); + // Default: no real network for the revocation panel — buttons remain + // idle until a test exercises them. getAdminCRLCache resolves to an + // empty rows array since the test mocks useAuth().admin = false. + vi.mocked(client.fetchCRL).mockResolvedValue({ byteLength: 1234, contentType: 'application/pkix-crl' } as never); + vi.mocked(client.getOCSPStatus).mockResolvedValue(new ArrayBuffer(256) as never); + vi.mocked(client.getAdminCRLCache).mockResolvedValue({ cache_rows: [], row_count: 0, generated_at: new Date().toISOString() } as never); }); it('renders the page when getCertificate resolves', async () => { @@ -114,3 +144,115 @@ describe('CertificateDetailPage — render + XSS hardening (M-026 / M-029 Pass 3 ).toBeUndefined(); }); }); + +// ----------------------------------------------------------------------------- +// CRL/OCSP-Responder Phase 5: Revocation Endpoints panel coverage. +// +// Pins: +// 1. The CRL distribution point + OCSP responder URLs render with the +// issuer_id substituted in (relying parties copy these straight into +// curl/openssl, so the format is load-bearing). +// 2. Clicking "Test CRL fetch" calls fetchCRL(issuer_id) and surfaces the +// byte-count success message — confirms the button is wired and not +// decorative. +// 3. Clicking "Check OCSP status" calls getOCSPStatus(issuer_id, serial) +// and surfaces the DER byte-count success message. +// 4. The admin cache-age badge stays HIDDEN when useAuth().admin is false +// (the hook is mocked to admin: false at the top of this file). Stops +// a regression where the badge silently leaks generation cadence to +// non-admin viewers. +// ----------------------------------------------------------------------------- + +describe('CertificateDetailPage — Revocation Endpoints panel (Phase 5)', () => { + const plainCert = { + id: 'mc-rev-001', + name: 'rev.example.com', + common_name: 'rev.example.com', + sans: ['rev.example.com'], + status: 'Active', + environment: 'prod', + issuer_id: 'iss-local-prod', + certificate_profile_id: 'cp-tls', + owner_id: 'o-ops', + team_id: 't-platform', + renewal_policy_id: 'rp-30d', + expires_at: new Date(Date.now() + 90 * 86400000).toISOString(), + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }; + + const certVersion = { + id: 'cv-1', + certificate_id: 'mc-rev-001', + serial_number: 'a1b2c3d4', + fingerprint_sha256: 'deadbeef'.repeat(8), + not_before: new Date(Date.now() - 86400000).toISOString(), + not_after: new Date(Date.now() + 90 * 86400000).toISOString(), + key_algorithm: 'ECDSA', + key_size: 256, + created_at: new Date().toISOString(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + cleanup(); + vi.mocked(client.getCertificate).mockResolvedValue(plainCert as never); + vi.mocked(client.getCertificateVersions).mockResolvedValue({ data: [certVersion], total: 1, page: 1, per_page: 50 } as never); + vi.mocked(client.getTargets).mockResolvedValue({ data: [], total: 0, page: 1, per_page: 50 } as never); + vi.mocked(client.getProfile).mockResolvedValue({ id: 'cp-tls', name: 'TLS' } as never); + vi.mocked(client.getProfiles).mockResolvedValue({ data: [], total: 0, page: 1, per_page: 50 } as never); + vi.mocked(client.getRenewalPolicies).mockResolvedValue({ data: [], total: 0, page: 1, per_page: 500 } as never); + vi.mocked(client.getJobs).mockResolvedValue({ data: [], total: 0, page: 1, per_page: 50 } as never); + vi.mocked(client.fetchCRL).mockResolvedValue({ byteLength: 4096, contentType: 'application/pkix-crl' } as never); + vi.mocked(client.getOCSPStatus).mockResolvedValue(new ArrayBuffer(312) as never); + vi.mocked(client.getAdminCRLCache).mockResolvedValue({ cache_rows: [], row_count: 0, generated_at: new Date().toISOString() } as never); + }); + + it('renders the CRL distribution point + OCSP responder URLs with the issuer_id substituted', async () => { + const { fireEvent: _fe } = await import('@testing-library/react'); + void _fe; + renderRoute(, '/certificates/mc-rev-001'); + await waitFor(() => { + expect(screen.getByRole('heading', { name: 'Revocation Endpoints' })).toBeInTheDocument(); + }); + + // Both URLs include the issuer_id segment under /.well-known/pki/. + // window.location.origin in jsdom is http://localhost:3000. + expect(screen.getByText('http://localhost:3000/.well-known/pki/crl/iss-local-prod')).toBeInTheDocument(); + expect(screen.getByText('http://localhost:3000/.well-known/pki/ocsp/iss-local-prod')).toBeInTheDocument(); + }); + + it('"Test CRL fetch" button calls fetchCRL(issuer_id) and shows the byte-count success message', async () => { + const { fireEvent } = await import('@testing-library/react'); + renderRoute(, '/certificates/mc-rev-001'); + const btn = await screen.findByRole('button', { name: /Test CRL fetch/i }); + fireEvent.click(btn); + await waitFor(() => { + expect(client.fetchCRL).toHaveBeenCalledWith('iss-local-prod'); + expect(screen.getByText(/OK — 4,096 bytes/)).toBeInTheDocument(); + }); + }); + + it('"Check OCSP status" button calls getOCSPStatus(issuer_id, serial_hex) and shows DER byte-count', async () => { + const { fireEvent } = await import('@testing-library/react'); + renderRoute(, '/certificates/mc-rev-001'); + const btn = await screen.findByRole('button', { name: /Check OCSP status/i }); + fireEvent.click(btn); + await waitFor(() => { + expect(client.getOCSPStatus).toHaveBeenCalledWith('iss-local-prod', 'a1b2c3d4'); + expect(screen.getByText(/OCSP response received — 312 bytes/)).toBeInTheDocument(); + }); + }); + + it('hides the admin cache-age badge when useAuth().admin is false (no information leak to non-admin)', async () => { + renderRoute(, '/certificates/mc-rev-001'); + await screen.findByRole('heading', { name: 'Revocation Endpoints' }); + // None of the badge variants ("Cache fresh" / "Cache stale" / "Not yet + // generated") should appear for a non-admin caller. + expect(screen.queryByText(/Cache fresh/i)).toBeNull(); + expect(screen.queryByText(/Cache stale/i)).toBeNull(); + expect(screen.queryByText(/Not yet generated/i)).toBeNull(); + // And the admin endpoint must not have been hit at all. + expect(client.getAdminCRLCache).not.toHaveBeenCalled(); + }); +}); diff --git a/web/src/pages/CertificateDetailPage.tsx b/web/src/pages/CertificateDetailPage.tsx index ad4f081..51dd9a1 100644 --- a/web/src/pages/CertificateDetailPage.tsx +++ b/web/src/pages/CertificateDetailPage.tsx @@ -2,13 +2,14 @@ import { useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; import { useTrackedMutation } from '../hooks/useTrackedMutation'; -import { getCertificate, getCertificateVersions, triggerRenewal, triggerDeployment, archiveCertificate, revokeCertificate, updateCertificate, getTargets, getJobs, getRenewalPolicies, getProfiles, getProfile, downloadCertificatePEM, exportCertificatePKCS12 } from '../api/client'; +import { getCertificate, getCertificateVersions, triggerRenewal, triggerDeployment, archiveCertificate, revokeCertificate, updateCertificate, getTargets, getJobs, getRenewalPolicies, getProfiles, getProfile, downloadCertificatePEM, exportCertificatePKCS12, getOCSPStatus, fetchCRL, getAdminCRLCache } from '../api/client'; import { REVOCATION_REASONS } from '../api/types'; import PageHeader from '../components/PageHeader'; import StatusBadge from '../components/StatusBadge'; import ErrorState from '../components/ErrorState'; +import { useAuth } from '../components/AuthProvider'; import { formatDate, formatDateTime, daysUntil, expiryColor, timeAgo } from '../api/utils'; -import type { Job } from '../api/types'; +import type { Job, CRLCacheRow } from '../api/types'; function InfoRow({ label, value, editable, onEdit }: { label: string; value: React.ReactNode; editable?: boolean; onEdit?: () => void }) { return ( @@ -159,6 +160,163 @@ function DeploymentTimeline({ certId, certStatus, createdAt, issuedAt }: { certI ); } +// CRL/OCSP-Responder Phase 5: Revocation Endpoints panel. +// +// Surfaces the standards-compliant revocation URLs (CRL distribution point +// per RFC 5280 §4.2.1.13, OCSP responder per RFC 6960 §A.1) for relying +// parties that don't already know certctl's well-known scheme. Both endpoints +// live under /.well-known/pki/ and run unauthenticated — relying-party clients +// should never need a Bearer key to check revocation status. +// +// The "Test CRL fetch" / "Check OCSP status" buttons exercise the same +// network path the CRL/OCSP responders advertise via the AIA + CDP +// extensions on issued leaves, so an operator confirming "did Phase 4 +// actually wire end-to-end?" can do it without curl. Failures bubble up +// as inline error text rather than throwing a global error boundary. +// +// The cache-age badge is admin-only (gated client-side AND server-side; the +// server returns 403 for non-admin even if the GUI bug-clicks the fetch). +// Stale rows render in amber per the IsStale flag (next_update < now). Rows +// missing entirely (issuer never had a CRL pre-generated) render the neutral +// "Not yet generated" pill. +function RevocationEndpointsCard({ issuerId, serialNumber }: { issuerId: string; serialNumber?: string }) { + const { admin } = useAuth(); + const [crlState, setCrlState] = useState<{ status: 'idle' | 'loading' | 'ok' | 'err'; msg?: string }>({ status: 'idle' }); + const [ocspState, setOcspState] = useState<{ status: 'idle' | 'loading' | 'ok' | 'err'; msg?: string }>({ status: 'idle' }); + + // Build the absolute URLs from window.location so operators can copy-paste + // them straight into curl / openssl. Using window.location keeps the URLs + // honest under reverse-proxy deployments where the perceived host differs + // from what the dev sees in their browser bar — the location object is the + // ground truth for "what URL does the relying party hit?". + const origin = typeof window !== 'undefined' ? window.location.origin : ''; + const crlURL = `${origin}/.well-known/pki/crl/${issuerId}`; + // OCSP per RFC 6960 §A.1.1 supports both POST (preferred for CSR-style + // requests) and the GET form with base64-url(DER) in the path. The GUI's + // "Check OCSP status" button uses the simpler /{issuer}/{serial_hex} + // helper certctl exposes alongside the standards endpoint — that's what + // getOCSPStatus() in client.ts hits. + const ocspURL = `${origin}/.well-known/pki/ocsp/${issuerId}`; + + // Admin-only: pull the cache row for this issuer so we can show + // "generated 2m ago / next update 58m" with a stale-warning chip. + const { data: cacheData } = useQuery({ + queryKey: ['admin-crl-cache'], + queryFn: () => getAdminCRLCache(), + enabled: admin, + // Refresh a touch faster than the default scheduler interval (1h) so + // the badge feels live during ops investigation. Falls back gracefully + // if the user navigates away before the next tick. + refetchInterval: 60_000, + retry: false, + }); + + const issuerRow: CRLCacheRow | undefined = cacheData?.cache_rows?.find(r => r.issuer_id === issuerId); + + const handleTestCRL = async () => { + setCrlState({ status: 'loading' }); + try { + const r = await fetchCRL(issuerId); + setCrlState({ status: 'ok', msg: `OK — ${r.byteLength.toLocaleString()} bytes (${r.contentType || 'no content-type'})` }); + } catch (e) { + setCrlState({ status: 'err', msg: e instanceof Error ? e.message : 'Fetch failed' }); + } + }; + + const handleCheckOCSP = async () => { + if (!serialNumber) { + setOcspState({ status: 'err', msg: 'Serial number unavailable — cert has not been issued yet.' }); + return; + } + setOcspState({ status: 'loading' }); + try { + const buf = await getOCSPStatus(issuerId, serialNumber); + setOcspState({ status: 'ok', msg: `OCSP response received — ${buf.byteLength.toLocaleString()} bytes (DER)` }); + } catch (e) { + setOcspState({ status: 'err', msg: e instanceof Error ? e.message : 'OCSP request failed' }); + } + }; + + return ( +
+
+

Revocation Endpoints

+ {admin && ( + issuerRow ? ( + issuerRow.cache_present ? ( + + {issuerRow.is_stale ? 'Cache stale' : 'Cache fresh'} + {issuerRow.generated_at ? ` · ${timeAgo(issuerRow.generated_at)}` : ''} + + ) : ( + + Not yet generated + + ) + ) : null + )} +
+ +
+
+
CRL Distribution Point (RFC 5280 §4.2.1.13)
+
+ {crlURL} + +
+ {crlState.status === 'ok' && ( +
{crlState.msg}
+ )} + {crlState.status === 'err' && ( +
{crlState.msg}
+ )} +
+ +
+
OCSP Responder (RFC 6960 §A.1)
+
+ {ocspURL} + +
+ {ocspState.status === 'ok' && ( +
{ocspState.msg}
+ )} + {ocspState.status === 'err' && ( +
{ocspState.msg}
+ )} + {!serialNumber && ocspState.status === 'idle' && ( +
Serial number unavailable — issue the cert first.
+ )} +
+
+ +

+ Both endpoints run unauthenticated under /.well-known/pki/ per RFC 8615 so relying parties can validate revocation without API keys. The CRL is pre-generated by the scheduler (configurable via CERTCTL_CRL_GENERATION_INTERVAL); OCSP is signed by the per-issuer responder cert (RFC 6960 §2.6). +

+
+ ); +} + function InlinePolicyEditor({ certId, currentPolicyId, currentProfileId }: { certId: string; currentPolicyId: string; currentProfileId: string }) { const [editing, setEditing] = useState(false); const [policyId, setPolicyId] = useState(currentPolicyId); @@ -613,6 +771,9 @@ export default function CertificateDetailPage() { currentProfileId={cert.certificate_profile_id || ''} /> + {/* Revocation Endpoints (CRL + OCSP) — Phase 5 */} + + {/* Tags */} {cert.tags && Object.keys(cert.tags).length > 0 && (