From 4dcc321be10730ee972e55a1a8044ed2d099f0a7 Mon Sep 17 00:00:00 2001 From: kayra1 Date: Fri, 23 Aug 2024 12:49:33 +0300 Subject: [PATCH] frontend update --- .../app/certificate_requests/components.tsx | 78 ++++++++++++------- ui/src/app/certificate_requests/row.tsx | 17 ++-- ui/src/app/utils.ts | 24 +++++- 3 files changed, 80 insertions(+), 39 deletions(-) diff --git a/ui/src/app/certificate_requests/components.tsx b/ui/src/app/certificate_requests/components.tsx index 529fc97..5cab055 100644 --- a/ui/src/app/certificate_requests/components.tsx +++ b/ui/src/app/certificate_requests/components.tsx @@ -1,7 +1,7 @@ -import { Dispatch, SetStateAction, useState, ChangeEvent } from "react" +import { Dispatch, SetStateAction, useState, ChangeEvent, useEffect } from "react" import { useMutation, useQueryClient } from "react-query" import { ConfirmationModalData } from "./row" -import { extractCert, csrMatchesCertificate } from "../utils" +import { csrMatchesCertificate, splitBundle, validateBundle } from "../utils" import { postCertToID } from "../queries" import { useCookies } from "react-cookie" @@ -33,27 +33,43 @@ export function ConfirmationModal({ modalData, setModalData }: ConfirmationModal } function SubmitCertificate({ existingCSRText, existingCertText, certText, onClickFunc }: { existingCSRText: string, existingCertText: string, certText: string, onClickFunc: any }) { - let certIsValid = false - let certMatchesCSR = false - try { - extractCert(certText) - certIsValid = true - if (csrMatchesCertificate(existingCSRText, certText)) { - certMatchesCSR = true + const [validationErrorText, setValidationErrorText] = useState("") + useEffect(() => { + const validateCertificate = async () => { + try { + const certs = splitBundle(certText) + if (certs.length < 2) { + setValidationErrorText("bundle with 2 certificates required") + return + } + if (!csrMatchesCertificate(existingCSRText, certs[0])) { + setValidationErrorText("Certificate does not match request") + return + } + let a = await validateBundle(certText) + if (await validateBundle(certText)) { + setValidationErrorText("Bundle validation failed: " + a) + return + } + } + catch { + setValidationErrorText("A certificate is invalid") + return + } + setValidationErrorText("") } - } - catch { } + validateCertificate() + }, [existingCSRText, existingCertText, certText]) - const validationComponent = certText == "" ? - <> : - !certIsValid ? -
Invalid Certificate
: - existingCertText == certText ? -
Certificate is identical to the one uploaded
: - !certMatchesCSR ? -
Certificate does not match the request
: -
Valid Certificate
- const buttonComponent = certIsValid && certMatchesCSR && existingCertText != certText ? : + const validationComponent = certText != "" && validationErrorText == "" ? ( +
Valid Certificate
+ ) : ( +
{validationErrorText}
) + const buttonComponent = validationErrorText == "" ? ( + + ) : ( + + ) return ( <> {validationComponent} @@ -70,11 +86,17 @@ interface SubmitCertificateModalProps { } export function SubmitCertificateModal({ id, csr, cert, setFormOpen }: SubmitCertificateModalProps) { const [cookies, setCookie, removeCookie] = useCookies(['user_token']); + const [errorText, setErrorText] = useState("") const queryClient = useQueryClient() const mutation = useMutation(postCertToID, { onSuccess: () => { queryClient.invalidateQueries('csrs') + setErrorText("") + setFormOpen(false) }, + onError: (e: Error) => { + setErrorText(e.message) + } }) const [certificatePEMString, setCertificatePEMString] = useState("") const handleTextChange = (event: ChangeEvent) => { @@ -94,10 +116,6 @@ export function SubmitCertificateModal({ id, csr, cert, setFormOpen }: SubmitCer reader.readAsText(file); } }; - const handleSubmit = () => { - mutation.mutate({ id: id, authToken: cookies.user_token, cert: certificatePEMString }) - setFormOpen(false) - } return (
- + {errorText != "" && +
+
+
Error
+

{errorText.split("error: ")}

+
+
+ } + mutation.mutate({ id: id, authToken: cookies.user_token, cert: splitBundle(certificatePEMString).join("\n") })} />
diff --git a/ui/src/app/certificate_requests/row.tsx b/ui/src/app/certificate_requests/row.tsx index e5d8d2a..780159e 100644 --- a/ui/src/app/certificate_requests/row.tsx +++ b/ui/src/app/certificate_requests/row.tsx @@ -1,6 +1,6 @@ import { useState, Dispatch, SetStateAction, useEffect, useRef } from "react" import { UseMutationResult, useMutation, useQueryClient } from "react-query" -import { extractCSR, extractCert } from "../utils" +import { extractCSR, extractCert, splitBundle } from "../utils" import { RequiredCSRParams, deleteCSR, rejectCSR, revokeCertificate } from "../queries" import { ConfirmationModal, SubmitCertificateModal, SuccessNotification } from "./components" import "./../globals.scss" @@ -31,7 +31,9 @@ export default function Row({ id, csr, certificate, ActionMenuExpanded, setActio const [confirmationModalData, setConfirmationModalData] = useState(null) const csrObj = extractCSR(csr) - const certObj = extractCert(certificate) + const certs = splitBundle(certificate) + const clientCertificate = certs.at(0) + const certObj = clientCertificate ? extractCert(clientCertificate) : null const queryClient = useQueryClient() const deleteMutation = useMutation(deleteCSR, { @@ -61,7 +63,7 @@ export default function Row({ id, csr, certificate, ActionMenuExpanded, setActio const blob = new Blob([csr], { type: 'text/plain' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); - link.download = "csr-" + id.toString() + ".pem"; // TODO: change this to .pem + link.download = "csr-" + (csrObj.commonName !== undefined ? csrObj.commonName : id.toString()) + ".pem"; document.body.appendChild(link); link.click(); document.body.removeChild(link); @@ -86,15 +88,6 @@ export default function Row({ id, csr, certificate, ActionMenuExpanded, setActio }) } - - const toggleActionMenu = () => { - if (ActionMenuExpanded == id) { - setActionMenuExpanded(0) - } else { - setActionMenuExpanded(id) - } - } - const getFieldDisplay = (label: string, field: string | undefined, compareField?: string | undefined) => { const isMismatched = compareField !== undefined && compareField !== field; return field ? ( diff --git a/ui/src/app/utils.ts b/ui/src/app/utils.ts index cfcfa77..51b2b4d 100644 --- a/ui/src/app/utils.ts +++ b/ui/src/app/utils.ts @@ -1,4 +1,4 @@ -import { CertificationRequest, Certificate, Extensions } from "pkijs"; +import { CertificationRequest, Certificate, Extensions, CertificateChainValidationEngine } from "pkijs"; import { fromBER } from "asn1js"; import * as pvutils from "pvutils"; @@ -275,4 +275,26 @@ export const passwordIsValid = (pw: string) => { return true } return false +} + +export const splitBundle = (bundle: string): string[] => { + const pemPattern = /-----BEGIN CERTIFICATE-----(?:.|\n)*?-----END CERTIFICATE-----/g; + const pemMatches = bundle.match(pemPattern); + return pemMatches ? pemMatches.map((e) => e.toString()) : []; +} + +export const validateBundle = async (bundle: string) => { + const bundleList = splitBundle(bundle) + const extractedCerts = bundleList.map((cert) => loadCertificate(cert)) + const rootCa = extractedCerts.at(-1) + if (rootCa == undefined) { + return "less than 2 certificates found." + } + const chainEngine = new CertificateChainValidationEngine({ + certs: extractedCerts, + trustedCerts: [rootCa] + }) + const result = await chainEngine.verify() + console.log(result) + return result.resultMessage } \ No newline at end of file