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 ++++++-- .../errorPage/SingleCertificateDetails.tsx | 369 +++++++++++++++++++++ .../errorPage/TrustCertificateDialog.tsx | 57 ++++ packages/renderer/src/stores/Service.ts | 6 + 4 files changed, 547 insertions(+), 21 deletions(-) create mode 100644 packages/renderer/src/components/errorPage/SingleCertificateDetails.tsx create mode 100644 packages/renderer/src/components/errorPage/TrustCertificateDialog.tsx (limited to 'packages/renderer') 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} + + )} ); diff --git a/packages/renderer/src/components/errorPage/SingleCertificateDetails.tsx b/packages/renderer/src/components/errorPage/SingleCertificateDetails.tsx new file mode 100644 index 0000000..107d9f1 --- /dev/null +++ b/packages/renderer/src/components/errorPage/SingleCertificateDetails.tsx @@ -0,0 +1,369 @@ +/* + * Copyright (C) 2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import AttachmentIcon from '@mui/icons-material/Attachment'; +import Link from '@mui/material/Link'; +import { styled } from '@mui/material/styles'; +import type { Certificate, CertificatePrincipal } from '@sophie/shared'; +import type { TFunction } from 'i18next'; +import { observer } from 'mobx-react-lite'; +import React, { type ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; + +const SingleCertificateDetailsRoot = styled('table')(({ theme }) => ({ + width: '100%', + borderSpacing: `${theme.spacing(2)} 0`, +})); + +const Header = styled('th')({ + textAlign: 'right', + whiteSpace: 'nowrap', + verticalAlign: 'top', +}); + +const SectionHeader = styled(Header)(({ theme }) => ({ + padding: `${theme.spacing(2)} 0`, +})); + +const SubHeader = styled(Header)(({ theme }) => ({ + color: theme.palette.text.secondary, +})); + +const Cell = styled('td')({ + verticalAlign: 'top', +}); + +function Section({ id, label }: { id: string; label: string }): JSX.Element { + return ( + + + {label} + +