From 1184eb13f0bbe4768bf3dbd6cd31adf10c6c8dfe Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Fri, 22 Apr 2022 00:53:48 +0200 Subject: feat: Certificate viewer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Show certificates with an interface modeled after firefox's certificate viewer so that they can be inspected before trusting. The current implementation assumes that each certificate has a unique fingerprint (collisions are astronomically unlikely). Signed-off-by: Kristóf Marussy --- .../components/errorPage/CertificateDetails.tsx | 136 +++++++++++++++++---- 1 file changed, 115 insertions(+), 21 deletions(-) (limited to 'packages/renderer/src/components/errorPage/CertificateDetails.tsx') diff --git a/packages/renderer/src/components/errorPage/CertificateDetails.tsx b/packages/renderer/src/components/errorPage/CertificateDetails.tsx index 044cb5c..51e7920 100644 --- a/packages/renderer/src/components/errorPage/CertificateDetails.tsx +++ b/packages/renderer/src/components/errorPage/CertificateDetails.tsx @@ -19,22 +19,41 @@ */ import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import WarningAmberIcon from '@mui/icons-material/WarningAmber'; import Accordion from '@mui/material/Accordion'; import AccordionDetails from '@mui/material/AccordionDetails'; import AccordionSummary from '@mui/material/AccordionSummary'; import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; import Typography from '@mui/material/Typography'; +import { styled } from '@mui/material/styles'; +import type { Certificate } from '@sophie/shared'; import { observer } from 'mobx-react-lite'; -import React from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import type Service from '../../stores/Service'; +import SingleCertificateDetails from './SingleCertificateDetails'; +import TrustCertificateDialog from './TrustCertificateDialog'; + const SUMMARY_ID = 'Sophie-CertificateDetails-header'; const DETAILS_ID = 'Sophie-CertificateDetails-content'; +function getCertificateChain(endEntityCertificate: Certificate): Certificate[] { + const chain: Certificate[] = []; + let certificate: Certificate | undefined = endEntityCertificate; + while (certificate !== undefined) { + chain.push(certificate); + certificate = certificate.issuerCert as Certificate; + } + return chain; +} + +const DetailsBox = styled(Box)(({ theme }) => ({ + padding: `0 ${theme.spacing(2)}`, +})); + function CertificateDetails({ service, }: { @@ -43,8 +62,9 @@ function CertificateDetails({ const { t } = useTranslation(undefined, { keyPrefix: 'error.certificateError.details', }); + const [certificateIndex, setCertificateIndex] = useState(0); - const { state } = service; + const { id: serviceId, state } = service; const { type: errorType } = state; if (errorType !== 'certificateError') { @@ -54,6 +74,22 @@ function CertificateDetails({ const { certificate, trust } = state; + const chain = getCertificateChain(certificate); + + if (chain.length === 0) { + // eslint-disable-next-line unicorn/no-null -- React requires `null` to skip rendering. + return null; + } + + const id = `${serviceId}-certificateDetails`; + + const trustCertificateDialog = ( + service.temporarilyTrustCurrentCertificate()} + /> + ); + return ( {t('title')} - - - {JSON.stringify(certificate)} - - - {t('warning')} - - - - + + {chain.length >= 2 ? ( + <> + ('tabsLabel')} + value={certificateIndex < chain.length ? certificateIndex : false} + onChange={(_event, value) => setCertificateIndex(value as number)} + variant="fullWidth" + > + {chain.map((member, i) => ( + + ))} + + ({ + paddingTop: 1, + borderTop: `1px solid ${theme.palette.divider}`, + })} + > + {chain.map((member, i) => ( + + ))} + + + ) : ( + + + service.downloadCertificate(chain[0].fingerprint) + } + /> + {trustCertificateDialog} + + )} ); -- cgit v1.2.3-54-g00ecf