fix(ui): match API whitespace stripping and lock the cert cap + extensions

This commit is contained in:
Lydia Vilchez
2026-09-02 13:17:05 +02:00
parent 788458be4e
commit d4990ce012
3 changed files with 105 additions and 6 deletions
+31
View File
@@ -96,6 +96,37 @@ describe("generateProwlerCertificate", () => {
writable: true,
});
});
it("emits a leaf cert with BasicConstraints(cA=false) and KeyUsage(digitalSignature)", async () => {
// Guardrail: audit tooling and strict CA validators flag self-signed
// leaves that omit these extensions. A future edit that drops them
// from the `extensions:` array must not slip past review.
const {
BasicConstraintsExtension,
KeyUsageFlags,
KeyUsagesExtension,
X509Certificate,
} = await import("@peculiar/x509");
const result = await generateProwlerCertificate({ modulusLength: 2048 });
const publicDer = Uint8Array.from(
atob(result.publicCertificateBase64Der),
(c) => c.charCodeAt(0),
);
const cert = new X509Certificate(publicDer);
const basicConstraints = cert.getExtension(BasicConstraintsExtension);
expect(basicConstraints).toBeDefined();
expect(basicConstraints!.ca).toBe(false);
const keyUsages = cert.getExtension(KeyUsagesExtension);
expect(keyUsages).toBeDefined();
// `usages` is a bitmask — verify the digitalSignature bit is set.
expect(keyUsages!.usages & KeyUsageFlags.digitalSignature).toBe(
KeyUsageFlags.digitalSignature,
);
});
});
describe("downloadPublicCertificateFile", () => {
+50
View File
@@ -7,7 +7,9 @@ import {
addCredentialsFormSchema,
addCredentialsRoleFormSchema,
addProviderFormSchema,
CERTIFICATE_CONTENT_MAX_SIZE_ERROR,
KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR,
MAX_CERTIFICATE_CONTENT_LENGTH,
samlConfigFormSchema,
} from "./formSchemas";
@@ -125,6 +127,54 @@ describe("addCredentialsFormSchema - azure certificate", () => {
// Then
expect(result.success).toBe(false);
});
it("rejects certificate content that exceeds the base64 length cap", () => {
// Guardrail against a future edit dropping the `.max(...)` on the
// Azure `certificate_content` field: a payload larger than the API's
// `_MAX_CERTIFICATE_CONTENT_LENGTH` must never leave the browser.
const schema = addCredentialsFormSchema("azure", "app_certificate");
// Padding-safe: 4-char multiple of "A" (all valid base64 chars) longer
// than the cap, so only the size check rejects (isValidBase64 passes).
const oversized = "A".repeat(MAX_CERTIFICATE_CONTENT_LENGTH + 4);
const result = schema.safeParse({
...BASE_AZURE_VALUES,
[ProviderCredentialFields.CERTIFICATE_CONTENT]: oversized,
});
expect(result.success).toBe(false);
if (result.success) return;
expect(result.error.issues).toContainEqual(
expect.objectContaining({
message: CERTIFICATE_CONTENT_MAX_SIZE_ERROR,
path: [ProviderCredentialFields.CERTIFICATE_CONTENT],
}),
);
});
it("accepts a wrapped-line certificate content that fits after stripping whitespace", () => {
// openssl and PowerShell wrap base64 output at 64 chars with LF/CRLF.
// The API strips whitespace before enforcing the cap; the client does
// the same via `.transform(strip)`. A legitimate ~50 KB base64 that
// exceeds the cap only because of embedded whitespace must still pass.
const schema = addCredentialsFormSchema("azure", "app_certificate");
const rawBase64 = "A".repeat(MAX_CERTIFICATE_CONTENT_LENGTH);
const wrapped = rawBase64.match(/.{1,64}/g)!.join("\r\n");
// Sanity: wrapping made it longer than the cap.
expect(wrapped.length).toBeGreaterThan(MAX_CERTIFICATE_CONTENT_LENGTH);
const result = schema.safeParse({
...BASE_AZURE_VALUES,
[ProviderCredentialFields.CERTIFICATE_CONTENT]: wrapped,
});
expect(result.success).toBe(true);
if (!result.success) return;
// The parsed value is the stripped base64 — matches what the API sees.
expect(
result.data[ProviderCredentialFields.CERTIFICATE_CONTENT],
).toBe(rawBase64);
});
});
describe("addProviderFormSchema - okta", () => {
+24 -6
View File
@@ -251,9 +251,18 @@ export const addCredentialsFormSchema = (
[ProviderCredentialFields.CLIENT_SECRET]: z.string().optional(),
[ProviderCredentialFields.CERTIFICATE_CONTENT]: z
.string()
.max(
MAX_CERTIFICATE_CONTENT_LENGTH,
CERTIFICATE_CONTENT_MAX_SIZE_ERROR,
// The API strips base64 whitespace before enforcing its
// 68268-char cap; measure the same value on the client so
// a legitimate CRLF-wrapped paste (openssl / PowerShell
// default line wrap) is not rejected as "too large".
.transform((value) => value.replace(/\s+/g, ""))
.pipe(
z
.string()
.max(
MAX_CERTIFICATE_CONTENT_LENGTH,
CERTIFICATE_CONTENT_MAX_SIZE_ERROR,
),
)
.optional(),
[ProviderCredentialFields.TENANT_ID]: z.guid({
@@ -298,9 +307,18 @@ export const addCredentialsFormSchema = (
.optional(),
[ProviderCredentialFields.CERTIFICATE_CONTENT]: z
.string()
.max(
MAX_CERTIFICATE_CONTENT_LENGTH,
CERTIFICATE_CONTENT_MAX_SIZE_ERROR,
// Same whitespace-then-cap contract as Azure — the
// API strips base64 whitespace before enforcing the
// 68268-char cap, so the client measures the same
// stripped value.
.transform((value) => value.replace(/\s+/g, ""))
.pipe(
z
.string()
.max(
MAX_CERTIFICATE_CONTENT_LENGTH,
CERTIFICATE_CONTENT_MAX_SIZE_ERROR,
),
)
.optional(),
[ProviderCredentialFields.TENANT_ID]: z