Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Extract certificate details #35

Merged
merged 10 commits into from
Jul 1, 2024
Merged
5 changes: 3 additions & 2 deletions ui/src/app/certificate_requests/row.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ test('Certificate Requests Table Row', () => {
<Row id={csr.ID} csr={csr.CSR} certificate={csr.Certificate} ActionMenuExpanded={actionMenuExpanded} setActionMenuExpanded={setActionMenuExpanded as Dispatch<SetStateAction<number>>} />
</QueryClientProvider>
)
expect(screen.getByText('10.152.183.53')).toBeDefined() // Common name of CSR
expect(screen.getByLabelText('certificate-expiry-date').innerHTML).toMatch(/^Thu Mar 27/)
const commonNames = screen.getAllByText('10.152.183.53');
expect(commonNames.length).toBeGreaterThan(0);
expect(screen.getByLabelText('certificate-expiry-date').innerHTML.trim()).toMatch(/^Thu Mar 27/)
const openActionsButton = screen.getByLabelText("action-menu-button")
fireEvent.click(openActionsButton);
expect(actionMenuExpanded).toBe(1)
Expand Down
83 changes: 65 additions & 18 deletions ui/src/app/certificate_requests/row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export type ConfirmationModalData = {
} | null

export default function Row({ id, csr, certificate, ActionMenuExpanded, setActionMenuExpanded }: rowProps) {
const red = "rgba(199, 22, 43, 1)"
const green = "rgba(14, 132, 32, 0.35)"
const yellow = "rgba(249, 155, 17, 0.45)"
const [successNotification, setSuccessNotification] = useState<string | null>(null)
const [detailsMenuOpen, setDetailsMenuOpen] = useState<boolean>(false)
const [certificateFormOpen, setCertificateFormOpen] = useState<boolean>(false)
Expand Down Expand Up @@ -88,13 +91,35 @@ export default function Row({ id, csr, certificate, ActionMenuExpanded, setActio
setActionMenuExpanded(id)
}
}
const getFieldDisplay = (key: string, field: string | undefined) => (
field ? (

const getFieldDisplay = (label: string, field: string | undefined, compareField?: string | undefined) => {
const isMismatched = compareField !== undefined && compareField !== field;
return field ? (
<p>
<b>{key}</b>: {field}
<b>{label}:</b>{" "}
<span style={{ color: isMismatched ? red : 'inherit' }}>
{field}
</span>
</p>
) : null
);
) : null;
};

const getExpiryColor = (notAfter?: string): string => {
if (!notAfter) return 'inherit';

const expiryDate = new Date(notAfter);
const now = new Date();
const oneDayInMillis = 24 * 60 * 60 * 1000;
const timeDifference = expiryDate.getTime() - now.getTime();

if (timeDifference < 0) {
return red;
} else if (timeDifference < oneDayInMillis) {
return yellow;
} else {
return green;
}
};

return (
<>
Expand All @@ -111,7 +136,7 @@ export default function Row({ id, csr, certificate, ActionMenuExpanded, setActio
<span>{csrObj.commonName}</span>
</td>
<td className="" aria-label="csr-status">{certificate == "" ? "outstanding" : (certificate == "rejected" ? "rejected" : "fulfilled")}</td>
<td className="" aria-label="certificate-expiry-date">{certificate == "" ? "" : (certificate == "rejected" ? "" : certObj?.notAfter)}</td>
<td className="" aria-label="certificate-expiry-date" style={{ backgroundColor: getExpiryColor(certObj?.notAfter) }}> {certificate === "" ? "" : (certificate === "rejected" ? "" : certObj?.notAfter)}</td>
<td className="has-overflow" data-heading="Actions">
<span className="p-contextual-menu--center u-no-margin--bottom">
<button
Expand Down Expand Up @@ -145,19 +170,41 @@ export default function Row({ id, csr, certificate, ActionMenuExpanded, setActio
</span>
</td>
<td id="expanded-row" className="p-table__expanding-panel" aria-hidden={detailsMenuOpen ? "false" : "true"}>
<div className="col-8">
<div className="certificate-info">
{getFieldDisplay("Common Name", csrObj.commonName)}
{getFieldDisplay("Subject Alternative Name DNS", csrObj.sansDns && csrObj.sansDns.length > 0 ? csrObj.sansDns.join(', ') : "")}
{getFieldDisplay("Subject Alternative Name IP addresses", csrObj.sansIp && csrObj.sansIp.length > 0 ? csrObj.sansIp.join(', ') : "")}
{getFieldDisplay("Country", csrObj.country)}
{getFieldDisplay("State or Province", csrObj.stateOrProvince)}
{getFieldDisplay("Locality", csrObj.locality)}
{getFieldDisplay("Organization", csrObj.organization)}
{getFieldDisplay("Organizational Unit", csrObj.OrganizationalUnitName)}
{getFieldDisplay("Email Address", csrObj.emailAddress)}
<p><b>Certificate request for a certificate authority</b>: {csrObj.is_ca ? "Yes" : "No"}</p>
<div className="row" style={{ display: 'flex', flexWrap: 'wrap', position: 'relative' }}>
<div className="col-12 col-md-6">
<div className="certificate-info">
<h4>Request Details</h4>
{getFieldDisplay("Common Name", csrObj.commonName)}
{getFieldDisplay("Subject Alternative Name DNS", csrObj.sansDns && csrObj.sansDns.length > 0 ? csrObj.sansDns.join(', ') : "")}
{getFieldDisplay("Subject Alternative Name IP addresses", csrObj.sansIp && csrObj.sansIp.length > 0 ? csrObj.sansIp.join(', ') : "")}
{getFieldDisplay("Country", csrObj.country)}
{getFieldDisplay("State or Province", csrObj.stateOrProvince)}
{getFieldDisplay("Locality", csrObj.locality)}
{getFieldDisplay("Organization", csrObj.organization)}
{getFieldDisplay("Organizational Unit", csrObj.OrganizationalUnitName)}
{getFieldDisplay("Email Address", csrObj.emailAddress)}
<p><b>Certificate request for a certificate authority</b>: {csrObj.is_ca ? "Yes" : "No"}</p>
</div>
</div>
{certObj && (certObj.notBefore || certObj.notAfter || certObj.issuerCommonName) && (
<div className="col-12 col-md-6">
<div className="certificate-info">
<h4>Certificate Details</h4>
{getFieldDisplay("Common Name", certObj.commonName, csrObj.commonName)}
{getFieldDisplay("Subject Alternative Name DNS", certObj.sansDns && certObj.sansDns.join(', '), csrObj.sansDns && csrObj.sansDns.join(', '))}
{getFieldDisplay("Subject Alternative Name IP addresses", certObj.sansIp && certObj.sansIp.join(', '), csrObj.sansIp && csrObj.sansIp.join(', '))}
{getFieldDisplay("Country", certObj.country, csrObj.country)}
{getFieldDisplay("State or Province", certObj.stateOrProvince, csrObj.stateOrProvince)}
{getFieldDisplay("Locality", certObj.locality, csrObj.locality)}
{getFieldDisplay("Organization", certObj.organization, csrObj.organization)}
{getFieldDisplay("Organizational Unit", certObj.OrganizationalUnitName, csrObj.OrganizationalUnitName)}
{getFieldDisplay("Email Address", certObj.emailAddress, csrObj.emailAddress)}
{getFieldDisplay("Start of validity", certObj.notBefore)}
{getFieldDisplay("Expiry Time", certObj.notAfter)}
{getFieldDisplay("Issuer Common Name", certObj.issuerCommonName)}
</div>
</div>
)}
</div>
</td>
</tr>
Expand Down
4 changes: 2 additions & 2 deletions ui/src/app/certificate_requests/table.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,6 @@ test('CertificateRequestsPage', () => {
< CertificateRequestsTable csrs={rows} />
</QueryClientProvider>
)
expect(screen.getByRole('table', {})).toBeDefined()
expect(screen.getByText('example.com')).toBeDefined() // Common Name of one of the CSR's
const commonNames = screen.getAllByText('example.com');
expect(commonNames.length).toBeGreaterThan(0);
})
145 changes: 84 additions & 61 deletions ui/src/app/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,25 @@ function parseExtensions(extensions: Extensions) {
return { sansDns, sansIp, is_ca };
}

export const extractCSR = (csrPemString: string) => {
const arrayBuffer = pemToArrayBuffer(csrPemString);
const asn1 = fromBER(arrayBuffer);
function loadCertificateRequest(csrPemString: string) {
const binaryDer = pemToArrayBuffer(csrPemString)
const asn1 = fromBER(binaryDer)
const csr = new CertificationRequest({ schema: asn1.result });
return csr
}

function loadCertificate(certPemString: string) {
const binaryDer = pemToArrayBuffer(certPemString)
const asn1 = fromBER(binaryDer)
if (asn1.offset === -1) {
throw new Error("Error parsing certificate");
}
const cert = new Certificate({ schema: asn1.result });
return cert
}

export const extractCSR = (csrPemString: string) => {
const csr = loadCertificateRequest(csrPemString);

// Extract subject information from CSR
const subjects = csr.subject.typesAndValues.map(typeAndValue => ({
Expand Down Expand Up @@ -136,76 +151,84 @@ export const extractCSR = (csrPemString: string) => {
}
}

export const extractCert = (certPemString: string) => {
if (certPemString == "" || certPemString == "rejected") { return }

// Decode PEM to DER
const binaryDer = pemToArrayBuffer(certPemString);

// Parse DER encoded certificate
const asn1 = fromBER(binaryDer);
if (asn1.offset === -1) {
throw new Error("Error parsing certificate");
export const extractCert = (certPemString: string) => {
if (certPemString === "" || certPemString === "rejected") {
return null;
}

// Load Certificate object
const cert = new Certificate({ schema: asn1.result });
const cert = loadCertificate(certPemString)

// Extract relevant information from certificate
const subject = cert.subject.typesAndValues.map(typeAndValue => ({
type: typeAndValue.type,
const subjects = cert.subject.typesAndValues.map(typeAndValue => ({
type: oidToName(typeAndValue.type),
value: typeAndValue.value.valueBlock.value
}));

const issuer = cert.issuer.typesAndValues.map(typeAndValue => ({
type: typeAndValue.type,
const issuerInfo = cert.subject.typesAndValues.map(typeAndValue => ({
type: oidToName(typeAndValue.type),
value: typeAndValue.value.valueBlock.value
}));
const getSubjectValue = (type: string) => subjects.find(subject => subject.type === type)?.value;
const getIssuerValue = (type: string) => issuerInfo.find(info => info.type === type)?.value;

const commonName = getSubjectValue("Common Name");
const organization = getSubjectValue("Organization Name");
const emailAddress = getSubjectValue("Email Address");
const country = getSubjectValue("Country");
const locality = getSubjectValue("Locality");
const stateOrProvince = getSubjectValue("State or Province");
const OrganizationalUnitName = getSubjectValue("Organizational Unit Name");
const issuerCommonName = getIssuerValue("Common Name");
const issuerOrganization = getIssuerValue("Organization Name");
const issuerEmailAddress = getIssuerValue("Email Address");
const issuerCountry = getIssuerValue("Country");
const issuerLocality = getIssuerValue("Locality");
const issuerStateOrProvince = getIssuerValue("State or Province");
const issuerOrganizationalUnitName = getIssuerValue("Organizational Unit Name");

const notBeforeUnformatted = cert.notBefore.value.toString();
const notBefore = notBeforeUnformatted ? notBeforeUnformatted.replace(/\s*\(.+\)/, '') : '';
const notAfterUnformatted = cert.notAfter.value.toString();
const notAfter = notAfterUnformatted ? notAfterUnformatted.replace(/\s*\(.+\)/, '') : '';

// Extract extensions such as SANs and Basic Constraints
let sansDns: string[] = [];
let sansIp: string[] = [];
let is_ca = false;

const notBefore = cert.notBefore.value.toString();
const notAfter = cert.notAfter.value.toString();

return { notAfter }
if (cert.extensions) {
// Correctly handle extensions by creating a new Extensions object
const extensionsInstance = new Extensions({ extensions: cert.extensions });
const ext = parseExtensions(extensionsInstance);
sansDns = ext.sansDns;
sansIp = ext.sansIp;
is_ca = ext.is_ca;
}
return {
commonName,
stateOrProvince,
OrganizationalUnitName,
organization,
emailAddress,
country,
locality,
sansDns,
sansIp,
is_ca,
notBefore,
notAfter,
issuerCommonName,
issuerOrganization,
issuerEmailAddress,
issuerCountry,
issuerLocality,
issuerStateOrProvince,
issuerOrganizationalUnitName,
};
}

export const csrMatchesCertificate = (csrPemString: string, certPemString: string) => {
// Decode PEM to DER
let pemHeader = "-----BEGIN CERTIFICATE-----";
let pemFooter = "-----END CERTIFICATE-----";
let pemContents = certPemString.substring(pemHeader.length, certPemString.length - pemFooter.length);
let binaryDerString = window.atob(pemContents);
let binaryDer = new Uint8Array(binaryDerString.length);
for (let i = 0; i < binaryDerString.length; i++) {
binaryDer[i] = binaryDerString.charCodeAt(i);
}

// Parse DER encoded certificate
let asn1 = fromBER(binaryDer.buffer);
if (asn1.offset === -1) {
throw new Error("Error parsing certificate");
}

// Load Certificate object
const cert = new Certificate({ schema: asn1.result });

// Decode PEM to DER
pemHeader = "-----BEGIN CERTIFICATE REQUEST-----";
pemFooter = "-----END CERTIFICATE REQUEST-----";
pemContents = csrPemString.substring(pemHeader.length, csrPemString.length - pemFooter.length);
binaryDerString = window.atob(pemContents);
binaryDer = new Uint8Array(binaryDerString.length);
for (let i = 0; i < binaryDerString.length; i++) {
binaryDer[i] = binaryDerString.charCodeAt(i);
}

// Parse DER encoded CSR
asn1 = fromBER(binaryDer.buffer);
if (asn1.offset === -1) {
throw new Error("Error parsing certificate request");
}

// Load CSR object
const csr = new CertificationRequest({ schema: asn1.result });
const cert = loadCertificate(certPemString);
const csr = loadCertificateRequest(csrPemString);

const csrPKbytes = csr.subjectPublicKeyInfo.subjectPublicKey.valueBeforeDecodeView
const certPKbytes = cert.subjectPublicKeyInfo.subjectPublicKey.valueBeforeDecodeView
Expand Down
Loading