diff options
author | Kristóf Marussy <kristof@marussy.com> | 2022-04-22 00:53:48 +0200 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2022-05-16 00:55:02 +0200 |
commit | 1184eb13f0bbe4768bf3dbd6cd31adf10c6c8dfe (patch) | |
tree | 78e6c699d8a232c948b40f643299f7af95a29215 /packages/renderer/src/components/errorPage/CertificateDetails.tsx | |
parent | feat(renderer): Insecure connection warning (diff) | |
download | sophie-1184eb13f0bbe4768bf3dbd6cd31adf10c6c8dfe.tar.gz sophie-1184eb13f0bbe4768bf3dbd6cd31adf10c6c8dfe.tar.zst sophie-1184eb13f0bbe4768bf3dbd6cd31adf10c6c8dfe.zip |
feat: Certificate viewer
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 <kristof@marussy.com>
Diffstat (limited to 'packages/renderer/src/components/errorPage/CertificateDetails.tsx')
-rw-r--r-- | packages/renderer/src/components/errorPage/CertificateDetails.tsx | 136 |
1 files changed, 115 insertions, 21 deletions
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 @@ | |||
19 | */ | 19 | */ |
20 | 20 | ||
21 | import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; | 21 | import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; |
22 | import WarningAmberIcon from '@mui/icons-material/WarningAmber'; | ||
23 | import Accordion from '@mui/material/Accordion'; | 22 | import Accordion from '@mui/material/Accordion'; |
24 | import AccordionDetails from '@mui/material/AccordionDetails'; | 23 | import AccordionDetails from '@mui/material/AccordionDetails'; |
25 | import AccordionSummary from '@mui/material/AccordionSummary'; | 24 | import AccordionSummary from '@mui/material/AccordionSummary'; |
26 | import Box from '@mui/material/Box'; | 25 | import Box from '@mui/material/Box'; |
27 | import Button from '@mui/material/Button'; | 26 | import Tab from '@mui/material/Tab'; |
27 | import Tabs from '@mui/material/Tabs'; | ||
28 | import Typography from '@mui/material/Typography'; | 28 | import Typography from '@mui/material/Typography'; |
29 | import { styled } from '@mui/material/styles'; | ||
30 | import type { Certificate } from '@sophie/shared'; | ||
29 | import { observer } from 'mobx-react-lite'; | 31 | import { observer } from 'mobx-react-lite'; |
30 | import React from 'react'; | 32 | import React, { useState } from 'react'; |
31 | import { useTranslation } from 'react-i18next'; | 33 | import { useTranslation } from 'react-i18next'; |
32 | 34 | ||
33 | import type Service from '../../stores/Service'; | 35 | import type Service from '../../stores/Service'; |
34 | 36 | ||
37 | import SingleCertificateDetails from './SingleCertificateDetails'; | ||
38 | import TrustCertificateDialog from './TrustCertificateDialog'; | ||
39 | |||
35 | const SUMMARY_ID = 'Sophie-CertificateDetails-header'; | 40 | const SUMMARY_ID = 'Sophie-CertificateDetails-header'; |
36 | const DETAILS_ID = 'Sophie-CertificateDetails-content'; | 41 | const DETAILS_ID = 'Sophie-CertificateDetails-content'; |
37 | 42 | ||
43 | function getCertificateChain(endEntityCertificate: Certificate): Certificate[] { | ||
44 | const chain: Certificate[] = []; | ||
45 | let certificate: Certificate | undefined = endEntityCertificate; | ||
46 | while (certificate !== undefined) { | ||
47 | chain.push(certificate); | ||
48 | certificate = certificate.issuerCert as Certificate; | ||
49 | } | ||
50 | return chain; | ||
51 | } | ||
52 | |||
53 | const DetailsBox = styled(Box)(({ theme }) => ({ | ||
54 | padding: `0 ${theme.spacing(2)}`, | ||
55 | })); | ||
56 | |||
38 | function CertificateDetails({ | 57 | function CertificateDetails({ |
39 | service, | 58 | service, |
40 | }: { | 59 | }: { |
@@ -43,8 +62,9 @@ function CertificateDetails({ | |||
43 | const { t } = useTranslation(undefined, { | 62 | const { t } = useTranslation(undefined, { |
44 | keyPrefix: 'error.certificateError.details', | 63 | keyPrefix: 'error.certificateError.details', |
45 | }); | 64 | }); |
65 | const [certificateIndex, setCertificateIndex] = useState(0); | ||
46 | 66 | ||
47 | const { state } = service; | 67 | const { id: serviceId, state } = service; |
48 | const { type: errorType } = state; | 68 | const { type: errorType } = state; |
49 | 69 | ||
50 | if (errorType !== 'certificateError') { | 70 | if (errorType !== 'certificateError') { |
@@ -54,6 +74,22 @@ function CertificateDetails({ | |||
54 | 74 | ||
55 | const { certificate, trust } = state; | 75 | const { certificate, trust } = state; |
56 | 76 | ||
77 | const chain = getCertificateChain(certificate); | ||
78 | |||
79 | if (chain.length === 0) { | ||
80 | // eslint-disable-next-line unicorn/no-null -- React requires `null` to skip rendering. | ||
81 | return null; | ||
82 | } | ||
83 | |||
84 | const id = `${serviceId}-certificateDetails`; | ||
85 | |||
86 | const trustCertificateDialog = ( | ||
87 | <TrustCertificateDialog | ||
88 | trust={trust} | ||
89 | onClick={() => service.temporarilyTrustCurrentCertificate()} | ||
90 | /> | ||
91 | ); | ||
92 | |||
57 | return ( | 93 | return ( |
58 | <Accordion | 94 | <Accordion |
59 | disableGutters | 95 | disableGutters |
@@ -67,23 +103,81 @@ function CertificateDetails({ | |||
67 | > | 103 | > |
68 | <Typography>{t('title')}</Typography> | 104 | <Typography>{t('title')}</Typography> |
69 | </AccordionSummary> | 105 | </AccordionSummary> |
70 | <AccordionDetails> | 106 | <AccordionDetails |
71 | <Typography sx={{ wordWrap: 'break-word' }}> | 107 | sx={{ |
72 | {JSON.stringify(certificate)} | 108 | px: 0, |
73 | </Typography> | 109 | pt: 0, |
74 | <Typography mt={2}> | 110 | }} |
75 | <WarningAmberIcon fontSize="small" /> <strong>{t('warning')}</strong> | 111 | > |
76 | </Typography> | 112 | {chain.length >= 2 ? ( |
77 | <Box mt={1}> | 113 | <> |
78 | <Button | 114 | <Tabs |
79 | disabled={trust !== 'pending'} | 115 | aria-label={t<string>('tabsLabel')} |
80 | onClick={() => service.temporarilyTrustCurrentCertificate()} | 116 | value={certificateIndex < chain.length ? certificateIndex : false} |
81 | color="error" | 117 | onChange={(_event, value) => setCertificateIndex(value as number)} |
82 | variant="outlined" | 118 | variant="fullWidth" |
83 | > | 119 | > |
84 | {t(`acceptButton.${trust}`)} | 120 | {chain.map((member, i) => ( |
85 | </Button> | 121 | <Tab |
86 | </Box> | 122 | key={member.fingerprint} |
123 | value={i} | ||
124 | id={`${id}-tab-${i}`} | ||
125 | aria-controls={`${id}-tabpanel-${i}`} | ||
126 | label={ | ||
127 | member.subjectName === '' | ||
128 | ? member.fingerprint | ||
129 | : member.subjectName | ||
130 | } | ||
131 | /> | ||
132 | ))} | ||
133 | </Tabs> | ||
134 | <DetailsBox | ||
135 | sx={(theme) => ({ | ||
136 | paddingTop: 1, | ||
137 | borderTop: `1px solid ${theme.palette.divider}`, | ||
138 | })} | ||
139 | > | ||
140 | {chain.map((member, i) => ( | ||
141 | <div | ||
142 | key={member.fingerprint} | ||
143 | role="tabpanel" | ||
144 | hidden={certificateIndex !== i} | ||
145 | id={`${id}-tabpanel-${i}`} | ||
146 | aria-labelledby={`${id}-tab-${i}`} | ||
147 | > | ||
148 | {certificateIndex === i && ( | ||
149 | <> | ||
150 | <SingleCertificateDetails | ||
151 | detailsId={id} | ||
152 | certificate={member} | ||
153 | onIssuerClick={ | ||
154 | i < chain.length - 1 | ||
155 | ? () => setCertificateIndex(i + 1) | ||
156 | : undefined | ||
157 | } | ||
158 | onDownloadClick={() => | ||
159 | service.downloadCertificate(member.fingerprint) | ||
160 | } | ||
161 | /> | ||
162 | {i === 0 && trustCertificateDialog} | ||
163 | </> | ||
164 | )} | ||
165 | </div> | ||
166 | ))} | ||
167 | </DetailsBox> | ||
168 | </> | ||
169 | ) : ( | ||
170 | <DetailsBox> | ||
171 | <SingleCertificateDetails | ||
172 | detailsId={id} | ||
173 | certificate={chain[0]} | ||
174 | onDownloadClick={() => | ||
175 | service.downloadCertificate(chain[0].fingerprint) | ||
176 | } | ||
177 | /> | ||
178 | {trustCertificateDialog} | ||
179 | </DetailsBox> | ||
180 | )} | ||
87 | </AccordionDetails> | 181 | </AccordionDetails> |
88 | </Accordion> | 182 | </Accordion> |
89 | ); | 183 | ); |