diff --git a/control-plane/src/api/renew.ts b/control-plane/src/api/renew.ts index 7851967..8d2c5a1 100644 --- a/control-plane/src/api/renew.ts +++ b/control-plane/src/api/renew.ts @@ -171,7 +171,15 @@ export function headerPresentedCert(headerName: string = DEFAULT_CLIENT_CERT_HEA const value = Array.isArray(raw) ? raw[0] : raw if (typeof value !== 'string' || value.length === 0) return null try { - return new Uint8Array(Buffer.from(value, 'base64')) + // The terminator may forward the verified cert as base64 DER, OR as PEM — including nginx's + // header-safe `$ssl_client_escaped_cert` (URL-encoded PEM). Normalize all three to raw DER: + // URL-decode if escaped, then strip PEM armor + whitespace to recover the base64 DER body. + let s = value.includes('%') ? safeDecodeUri(value) : value + if (s.includes('BEGIN CERTIFICATE')) { + s = s.replace(/-----[^-]+-----/g, '').replace(/\s+/g, '') + } + const der = Buffer.from(s, 'base64') + return der.length > 0 ? new Uint8Array(der) : null } catch { return null } @@ -179,6 +187,15 @@ export function headerPresentedCert(headerName: string = DEFAULT_CLIENT_CERT_HEA } } +/** URL-decode `value`, returning it unchanged if it is not valid percent-encoding. */ +function safeDecodeUri(value: string): string { + try { + return decodeURIComponent(value) + } catch { + return value + } +} + /** * Trust-anchor verification of the presented current cert (defense in depth: the mTLS terminator has * already verified it, but this route must not trust a cert the terminator would never have accepted).