| // Copyright 2015 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| /* eslint-disable @devtools/no-imperative-dom-api */ |
| |
| import * as Common from '../../core/common/common.js'; |
| import * as Host from '../../core/host/host.js'; |
| import * as i18n from '../../core/i18n/i18n.js'; |
| import type * as Platform from '../../core/platform/platform.js'; |
| import * as SDK from '../../core/sdk/sdk.js'; |
| import * as Protocol from '../../generated/protocol.js'; |
| import * as NetworkForward from '../../panels/network/forward/forward.js'; |
| import {createIcon, type Icon} from '../../ui/kit/kit.js'; |
| import * as UI from '../../ui/legacy/legacy.js'; |
| import {html, render} from '../../ui/lit/lit.js'; |
| import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; |
| |
| import lockIconStyles from './lockIcon.css.js'; |
| import mainViewStyles from './mainView.css.js'; |
| import {ShowOriginEvent} from './OriginTreeElement.js'; |
| import originViewStyles from './originView.css.js'; |
| import { |
| Events, |
| type PageVisibleSecurityState, |
| SecurityModel, |
| securityStateCompare, |
| SecurityStyleExplanation, |
| SummaryMessages, |
| } from './SecurityModel.js'; |
| import {SecurityPanelSidebar} from './SecurityPanelSidebar.js'; |
| |
| const {widgetConfig} = UI.Widget; |
| |
| const UIStrings = { |
| /** |
| * @description Summary div text content in Security Panel of the Security panel |
| */ |
| securityOverview: 'Security overview', |
| /** |
| * @description Text to show something is secure |
| */ |
| secure: 'Secure', |
| /** |
| * @description Sdk console message message level info of level Labels in Console View of the Console panel |
| */ |
| info: 'Info', |
| /** |
| * @description Not secure div text content in Security Panel of the Security panel |
| */ |
| notSecure: 'Not secure', |
| /** |
| * @description Text to view a security certificate |
| */ |
| viewCertificate: 'View certificate', |
| /** |
| * @description Text in Security Panel of the Security panel |
| */ |
| notSecureBroken: 'Not secure (broken)', |
| /** |
| * @description Main summary for page when it has been deemed unsafe by the SafeBrowsing service. |
| */ |
| thisPageIsDangerousFlaggedBy: 'This page is dangerous (flagged by Google Safe Browsing).', |
| /** |
| * @description Summary phrase for a security problem where the site is deemed unsafe by the SafeBrowsing service. |
| */ |
| flaggedByGoogleSafeBrowsing: 'Flagged by Google Safe Browsing', |
| /** |
| * @description Description of a security problem where the site is deemed unsafe by the SafeBrowsing service. |
| */ |
| toCheckThisPagesStatusVisit: 'To check this page\'s status, visit g.co/safebrowsingstatus.', |
| /** |
| * @description Main summary for a non cert error page. |
| */ |
| thisIsAnErrorPage: 'This is an error page.', |
| /** |
| * @description Main summary for where the site is non-secure HTTP. |
| */ |
| thisPageIsInsecureUnencrypted: 'This page is insecure (unencrypted HTTP).', |
| /** |
| * @description Main summary for where the site has a non-cryptographic secure origin. |
| */ |
| thisPageHasANonhttpsSecureOrigin: 'This page has a non-HTTPS secure origin.', |
| /** |
| * @description Message to display in devtools security tab when the page you are on triggered a safety tip. |
| */ |
| thisPageIsSuspicious: 'This page is suspicious', |
| /** |
| * @description Body of message to display in devtools security tab when you are viewing a page that triggered a safety tip. |
| */ |
| chromeHasDeterminedThatThisSiteS: 'Chrome has determined that this site could be fake or fraudulent.', |
| /** |
| * @description Second part of the body of message to display in devtools security tab when you are viewing a page that triggered a safety tip. |
| */ |
| ifYouBelieveThisIsShownIn: |
| 'If you believe this is shown in error please visit https://g.co/chrome/lookalike-warnings.', |
| /** |
| * @description Summary of a warning when the user visits a page that triggered a Safety Tip because the domain looked like another domain. |
| */ |
| possibleSpoofingUrl: 'Possible spoofing URL', |
| /** |
| * @description Body of a warning when the user visits a page that triggered a Safety Tip because the domain looked like another domain. |
| * @example {wikipedia.org} PH1 |
| */ |
| thisSitesHostnameLooksSimilarToP: |
| 'This site\'s hostname looks similar to {PH1}. Attackers sometimes mimic sites by making small, hard-to-see changes to the domain name.', |
| /** |
| * @description second part of body of a warning when the user visits a page that triggered a Safety Tip because the domain looked like another domain. |
| */ |
| ifYouBelieveThisIsShownInErrorSafety: |
| 'If you believe this is shown in error please visit https://g.co/chrome/lookalike-warnings.', |
| /** |
| * @description Title of the devtools security tab when the page you are on triggered a safety tip. |
| */ |
| thisPageIsSuspiciousFlaggedBy: 'This page is suspicious (flagged by Chrome).', |
| /** |
| * @description Text for a security certificate |
| */ |
| certificate: 'Certificate', |
| /** |
| * @description Summary phrase for a security problem where the site's certificate chain contains a SHA1 signature. |
| */ |
| insecureSha: 'insecure (SHA-1)', |
| /** |
| * @description Description of a security problem where the site's certificate chain contains a SHA1 signature. |
| */ |
| theCertificateChainForThisSite: 'The certificate chain for this site contains a certificate signed using SHA-1.', |
| /** |
| * @description Summary phrase for a security problem where the site's certificate is missing a subjectAltName extension. |
| */ |
| subjectAlternativeNameMissing: '`Subject Alternative Name` missing', |
| /** |
| * @description Description of a security problem where the site's certificate is missing a subjectAltName extension. |
| */ |
| theCertificateForThisSiteDoesNot: |
| 'The certificate for this site does not contain a `Subject Alternative Name` extension containing a domain name or IP address.', |
| /** |
| * @description Summary phrase for a security problem with the site's certificate. |
| */ |
| missing: 'missing', |
| /** |
| * @description Description of a security problem with the site's certificate. |
| * @example {net::ERR_CERT_AUTHORITY_INVALID} PH1 |
| */ |
| thisSiteIsMissingAValidTrusted: 'This site is missing a valid, trusted certificate ({PH1}).', |
| /** |
| * @description Summary phrase for a site that has a valid server certificate. |
| */ |
| validAndTrusted: 'valid and trusted', |
| /** |
| * @description Description of a site that has a valid server certificate. |
| * @example {Let's Encrypt Authority X3} PH1 |
| */ |
| theConnectionToThisSiteIsUsingA: |
| 'The connection to this site is using a valid, trusted server certificate issued by {PH1}.', |
| /** |
| * @description Summary phrase for a security state where Private Key Pinning is ignored because the certificate chains to a locally-trusted root. |
| */ |
| publickeypinningBypassed: 'Public-Key-Pinning bypassed', |
| /** |
| * @description Description of a security state where Private Key Pinning is ignored because the certificate chains to a locally-trusted root. |
| */ |
| publickeypinningWasBypassedByA: 'Public-Key-Pinning was bypassed by a local root certificate.', |
| /** |
| * @description Summary phrase for a site with a certificate that is expiring soon. |
| */ |
| certificateExpiresSoon: 'Certificate expires soon', |
| /** |
| * @description Description for a site with a certificate that is expiring soon. |
| */ |
| theCertificateForThisSiteExpires: |
| 'The certificate for this site expires in less than 48 hours and needs to be renewed.', |
| /** |
| * @description Text that refers to the network connection |
| */ |
| connection: 'Connection', |
| /** |
| * @description Summary phrase for a site that uses a modern, secure TLS protocol and cipher. |
| */ |
| secureConnectionSettings: 'secure connection settings', |
| /** |
| * @description Description of a site's TLS settings. |
| * @example {TLS 1.2} PH1 |
| * @example {ECDHE_RSA} PH2 |
| * @example {AES_128_GCM} PH3 |
| */ |
| theConnectionToThisSiteIs: |
| 'The connection to this site is encrypted and authenticated using {PH1}, {PH2}, and {PH3}.', |
| /** |
| * @description A recommendation to the site owner to use a modern TLS protocol |
| * @example {TLS 1.0} PH1 |
| */ |
| sIsObsoleteEnableTlsOrLater: '{PH1} is obsolete. Enable TLS 1.2 or later.', |
| /** |
| * @description A recommendation to the site owner to use a modern TLS key exchange |
| */ |
| rsaKeyExchangeIsObsoleteEnableAn: 'RSA key exchange is obsolete. Enable an ECDHE-based cipher suite.', |
| /** |
| * @description A recommendation to the site owner to use a modern TLS cipher |
| * @example {3DES_EDE_CBC} PH1 |
| */ |
| sIsObsoleteEnableAnAesgcmbased: '{PH1} is obsolete. Enable an AES-GCM-based cipher suite.', |
| /** |
| * @description A recommendation to the site owner to use a modern TLS server signature |
| */ |
| theServerSignatureUsesShaWhichIs: |
| 'The server signature uses SHA-1, which is obsolete. Enable a SHA-2 signature algorithm instead. (Note this is different from the signature in the certificate.)', |
| /** |
| * @description Summary phrase for a site that uses an outdated SSL settings (protocol, key exchange, or cipher). |
| */ |
| obsoleteConnectionSettings: 'obsolete connection settings', |
| /** |
| * @description A title of the 'Resources' action category |
| */ |
| resources: 'Resources', |
| /** |
| * @description Summary for page when there is active mixed content |
| */ |
| activeMixedContent: 'active mixed content', |
| /** |
| * @description Description for page when there is active mixed content |
| */ |
| youHaveRecentlyAllowedNonsecure: |
| 'You have recently allowed non-secure content (such as scripts or iframes) to run on this site.', |
| /** |
| * @description Summary for page when there is mixed content |
| */ |
| mixedContent: 'mixed content', |
| /** |
| * @description Description for page when there is mixed content |
| */ |
| thisPageIncludesHttpResources: 'This page includes HTTP resources.', |
| /** |
| * @description Summary for page when there is a non-secure form |
| */ |
| nonsecureForm: 'non-secure form', |
| /** |
| * @description Description for page when there is a non-secure form |
| */ |
| thisPageIncludesAFormWithA: 'This page includes a form with a non-secure "action" attribute.', |
| /** |
| * @description Summary for the page when it contains active content with certificate error |
| */ |
| activeContentWithCertificate: 'active content with certificate errors', |
| /** |
| * @description Description for the page when it contains active content with certificate error |
| */ |
| youHaveRecentlyAllowedContent: |
| 'You have recently allowed content loaded with certificate errors (such as scripts or iframes) to run on this site.', |
| /** |
| * @description Summary for page when there is active content with certificate errors |
| */ |
| contentWithCertificateErrors: 'content with certificate errors', |
| /** |
| * @description Description for page when there is content with certificate errors |
| */ |
| thisPageIncludesResourcesThat: 'This page includes resources that were loaded with certificate errors.', |
| /** |
| * @description Summary for page when all resources are served securely |
| */ |
| allServedSecurely: 'all served securely', |
| /** |
| * @description Description for page when all resources are served securely |
| */ |
| allResourcesOnThisPageAreServed: 'All resources on this page are served securely.', |
| /** |
| * @description Text in Security Panel of the Security panel |
| */ |
| blockedMixedContent: 'Blocked mixed content', |
| /** |
| * @description Text in Security Panel of the Security panel |
| */ |
| yourPageRequestedNonsecure: 'Your page requested non-secure resources that were blocked.', |
| /** |
| * @description Refresh prompt text content in Security Panel of the Security panel |
| */ |
| reloadThePageToRecordRequestsFor: 'Reload the page to record requests for HTTP resources.', |
| /** |
| * @description Link text in the Security Panel. Clicking the link navigates the user to the |
| * Network panel. Requests refers to network requests. Each request is a piece of data transmitted |
| * from the current user's browser to a remote server. |
| */ |
| viewDRequestsInNetworkPanel: |
| '{n, plural, =1 {View # request in Network Panel} other {View # requests in Network Panel}}', |
| /** |
| * @description Text for the origin of something |
| */ |
| origin: 'Origin', |
| /** |
| * @description Text in Security Panel of the Security panel |
| */ |
| viewRequestsInNetworkPanel: 'View requests in Network Panel', |
| /** |
| * @description Text for security or network protocol |
| */ |
| protocol: 'Protocol', |
| /** |
| * @description Text in the Security panel that refers to how the TLS handshake |
| *established encryption keys. |
| */ |
| keyExchange: 'Key exchange', |
| /** |
| * @description Text in Security Panel that refers to how the TLS handshake |
| *encrypted data. |
| */ |
| cipher: 'Cipher', |
| /** |
| * @description Text in Security Panel that refers to the signature algorithm |
| *used by the server for authenticate in the TLS handshake. |
| */ |
| serverSignature: 'Server signature', |
| /** |
| * @description Text in Security Panel that refers to whether the ClientHello |
| *message in the TLS handshake was encrypted. |
| */ |
| encryptedClientHello: 'Encrypted ClientHello', |
| /** |
| * @description Sct div text content in Security Panel of the Security panel |
| */ |
| certificateTransparency: 'Certificate Transparency', |
| /** |
| * @description Text that refers to the subject of a security certificate |
| */ |
| subject: 'Subject', |
| /** |
| * @description Text to show since when an item is valid |
| */ |
| validFrom: 'Valid from', |
| /** |
| * @description Text to indicate the expiry date |
| */ |
| validUntil: 'Valid until', |
| /** |
| * @description Text for the issuer of an item |
| */ |
| issuer: 'Issuer', |
| /** |
| * @description Text in Security Panel of the Security panel |
| */ |
| openFullCertificateDetails: 'Open full certificate details', |
| /** |
| * @description Text in Security Panel of the Security panel |
| */ |
| sct: 'SCT', |
| /** |
| * @description Text in Security Panel of the Security panel |
| */ |
| logName: 'Log name', |
| /** |
| * @description Text in Security Panel of the Security panel |
| */ |
| logId: 'Log ID', |
| /** |
| * @description Text in Security Panel of the Security panel |
| */ |
| validationStatus: 'Validation status', |
| /** |
| * @description Text for the source of something |
| */ |
| source: 'Source', |
| /** |
| * @description Label for a date/time string in the Security panel. It indicates the time at which |
| * a security certificate was issued (created by an authority and distributed). |
| */ |
| issuedAt: 'Issued at', |
| /** |
| * @description Text in Security Panel of the Security panel |
| */ |
| hashAlgorithm: 'Hash algorithm', |
| /** |
| * @description Text in Security Panel of the Security panel |
| */ |
| signatureAlgorithm: 'Signature algorithm', |
| /** |
| * @description Text in Security Panel of the Security panel |
| */ |
| signatureData: 'Signature data', |
| /** |
| * @description Toggle scts details link text content in Security Panel of the Security panel |
| */ |
| showFullDetails: 'Show full details', |
| /** |
| * @description Toggle scts details link text content in Security Panel of the Security panel |
| */ |
| hideFullDetails: 'Hide full details', |
| /** |
| * @description Text in Security Panel of the Security panel |
| */ |
| thisRequestCompliesWithChromes: 'This request complies with `Chrome`\'s Certificate Transparency policy.', |
| /** |
| * @description Text in Security Panel of the Security panel |
| */ |
| thisRequestDoesNotComplyWith: 'This request does not comply with `Chrome`\'s Certificate Transparency policy.', |
| /** |
| * @description Text in Security Panel of the Security panel |
| */ |
| thisResponseWasLoadedFromCache: 'This response was loaded from cache. Some security details might be missing.', |
| /** |
| * @description Text in Security Panel of the Security panel |
| */ |
| theSecurityDetailsAboveAreFrom: 'The security details above are from the first inspected response.', |
| /** |
| * @description Main summary for where the site has a non-cryptographic secure origin. |
| */ |
| thisOriginIsANonhttpsSecure: 'This origin is a non-HTTPS secure origin.', |
| /** |
| * @description Text in Security Panel of the Security panel |
| */ |
| yourConnectionToThisOriginIsNot: 'Your connection to this origin is not secure.', |
| /** |
| * @description No info div text content in Security Panel of the Security panel |
| */ |
| noSecurityInformation: 'No security information', |
| /** |
| * @description Text in Security Panel of the Security panel |
| */ |
| noSecurityDetailsAreAvailableFor: 'No security details are available for this origin.', |
| /** |
| * @description San div text content in Security Panel of the Security panel |
| */ |
| na: '(n/a)', |
| /** |
| * @description Text to show less content |
| */ |
| showLess: 'Show less', |
| /** |
| * @description Truncated santoggle text content in Security Panel of the Security panel |
| * @example {2} PH1 |
| */ |
| showMoreSTotal: 'Show more ({PH1} total)', |
| /** |
| * @description Shown when a field refers to an option that is unknown to the frontend. |
| */ |
| unknownField: 'unknown', |
| /** |
| * @description Shown when a field refers to a TLS feature which was enabled. |
| */ |
| enabled: 'enabled', |
| } as const; |
| const str_ = i18n.i18n.registerUIStrings('panels/security/SecurityPanel.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| |
| let securityPanelInstance: SecurityPanel; |
| |
| // See https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-signaturescheme |
| // This contains signature schemes supported by Chrome. |
| const SignatureSchemeStrings = new Map([ |
| // The full name for these schemes is RSASSA-PKCS1-v1_5, sometimes |
| // "PKCS#1 v1.5", but those are very long, so let "RSA" vs "RSA-PSS" |
| // disambiguate. |
| [0x0201, 'RSA with SHA-1'], |
| [0x0401, 'RSA with SHA-256'], |
| [0x0501, 'RSA with SHA-384'], |
| [0x0601, 'RSA with SHA-512'], |
| |
| // We omit the curve from these names because in TLS 1.2 these code points |
| // were not specific to a curve. Saying "P-256" for a server that used a P-384 |
| // key with SHA-256 in TLS 1.2 would be confusing. |
| [0x0403, 'ECDSA with SHA-256'], |
| [0x0503, 'ECDSA with SHA-384'], |
| |
| [0x0804, 'RSA-PSS with SHA-256'], |
| [0x0805, 'RSA-PSS with SHA-384'], |
| [0x0806, 'RSA-PSS with SHA-512'], |
| ]); |
| |
| const LOCK_ICON_NAME = 'lock'; |
| const WARNING_ICON_NAME = 'warning'; |
| const UNKNOWN_ICON_NAME = 'indeterminate-question-box'; |
| |
| export function getSecurityStateIconForDetailedView( |
| securityState: Protocol.Security.SecurityState, className: string): Icon { |
| let iconName: string; |
| |
| switch (securityState) { |
| case Protocol.Security.SecurityState.Neutral: // fallthrough |
| case Protocol.Security.SecurityState.Insecure: // fallthrough |
| case Protocol.Security.SecurityState.InsecureBroken: |
| iconName = WARNING_ICON_NAME; |
| break; |
| case Protocol.Security.SecurityState.Secure: |
| iconName = LOCK_ICON_NAME; |
| break; |
| case Protocol.Security.SecurityState.Info: // fallthrough |
| case Protocol.Security.SecurityState.Unknown: |
| iconName = UNKNOWN_ICON_NAME; |
| break; |
| } |
| |
| return createIcon(iconName, className); |
| } |
| |
| export function getSecurityStateIconForOverview( |
| securityState: Protocol.Security.SecurityState, className: string): Icon { |
| let iconName: string; |
| switch (securityState) { |
| case Protocol.Security.SecurityState.Unknown: // fallthrough |
| case Protocol.Security.SecurityState.Neutral: |
| iconName = UNKNOWN_ICON_NAME; |
| break; |
| case Protocol.Security.SecurityState.Insecure: // fallthrough |
| case Protocol.Security.SecurityState.InsecureBroken: |
| iconName = WARNING_ICON_NAME; |
| break; |
| case Protocol.Security.SecurityState.Secure: |
| iconName = LOCK_ICON_NAME; |
| break; |
| case Protocol.Security.SecurityState.Info: |
| throw new Error(`Unexpected security state ${securityState}`); |
| } |
| return createIcon(iconName, className); |
| } |
| |
| export function createHighlightedUrl(url: Platform.DevToolsPath.UrlString, securityState: string): Element { |
| const schemeSeparator = '://'; |
| const index = url.indexOf(schemeSeparator); |
| |
| // If the separator is not found, just display the text without highlighting. |
| if (index === -1) { |
| const text = document.createElement('span'); |
| text.textContent = url; |
| return text; |
| } |
| |
| const highlightedUrl = document.createElement('span'); |
| highlightedUrl.classList.add('highlighted-url'); |
| const scheme = url.substr(0, index); |
| const content = url.substr(index + schemeSeparator.length); |
| highlightedUrl.createChild('span', 'url-scheme-' + securityState).textContent = scheme; |
| highlightedUrl.createChild('span', 'url-scheme-separator').textContent = schemeSeparator; |
| highlightedUrl.createChild('span').textContent = content; |
| |
| return highlightedUrl; |
| } |
| |
| export interface ViewInput { |
| panel: SecurityPanel; |
| } |
| export interface ViewOutput { |
| setVisibleView: (view: UI.Widget.VBox) => void; |
| splitWidget: UI.SplitWidget.SplitWidget; |
| mainView: SecurityMainView; |
| visibleView: UI.Widget.VBox|null; |
| sidebar: SecurityPanelSidebar; |
| } |
| |
| export type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void; |
| |
| const DEFAULT_VIEW: View = (input: ViewInput, output: ViewOutput, target: HTMLElement): void => { |
| // clang-format off |
| render(html` |
| <devtools-split-view direction="column" name="security" |
| ${UI.Widget.widgetRef(UI.SplitWidget.SplitWidget, e => {output.splitWidget = e;})}> |
| <devtools-widget |
| slot="sidebar" |
| .widgetConfig=${widgetConfig(SecurityPanelSidebar)} |
| ${UI.Widget.widgetRef(SecurityPanelSidebar, e => {output.sidebar = e;})}> |
| </devtools-widget> |
| </devtools-split-view>`, |
| target); |
| // clang-format on |
| }; |
| |
| export class SecurityPanel extends UI.Panel.Panel implements SDK.TargetManager.SDKModelObserver<SecurityModel> { |
| readonly mainView: SecurityMainView; |
| readonly sidebar!: SecurityPanelSidebar; |
| private readonly lastResponseReceivedForLoaderId: Map<string, SDK.NetworkRequest.NetworkRequest>; |
| private readonly origins: Map<string, OriginState>; |
| private readonly filterRequestCounts: Map<string, number>; |
| visibleView: UI.Widget.VBox|null; |
| private eventListeners: Common.EventTarget.EventDescriptor[]; |
| private securityModel: SecurityModel|null; |
| readonly splitWidget!: UI.SplitWidget.SplitWidget; |
| |
| constructor(private view: View = DEFAULT_VIEW) { |
| super('security'); |
| |
| this.update(); |
| |
| this.sidebar.setMinimumSize(100, 25); |
| this.sidebar.element.classList.add('panel-sidebar'); |
| this.sidebar.element.setAttribute('jslog', `${VisualLogging.pane('sidebar').track({resize: true})}`); |
| |
| this.mainView = new SecurityMainView(); |
| this.mainView.panel = this; |
| this.element.addEventListener(ShowOriginEvent.eventName, (event: ShowOriginEvent) => { |
| if (event.origin) { |
| this.showOrigin(event.origin); |
| } else { |
| this.setVisibleView(this.mainView); |
| } |
| }); |
| |
| this.lastResponseReceivedForLoaderId = new Map(); |
| |
| this.origins = new Map(); |
| |
| this.filterRequestCounts = new Map(); |
| |
| this.visibleView = null; |
| this.eventListeners = []; |
| this.securityModel = null; |
| |
| SDK.TargetManager.TargetManager.instance().observeModels(SecurityModel, this, {scoped: true}); |
| SDK.TargetManager.TargetManager.instance().addModelListener( |
| SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.PrimaryPageChanged, |
| this.onPrimaryPageChanged, this); |
| |
| this.sidebar.showLastSelectedElement(); |
| } |
| |
| static instance(opts: {forceNew: boolean|null} = {forceNew: null}): SecurityPanel { |
| const {forceNew} = opts; |
| if (!securityPanelInstance || forceNew) { |
| securityPanelInstance = new SecurityPanel(); |
| } |
| |
| return securityPanelInstance; |
| } |
| |
| static createCertificateViewerButtonForOrigin(text: string, origin: string): Element { |
| const certificateButton = UI.UIUtils.createTextButton(text, async (e: Event) => { |
| e.consume(); |
| const names = await SDK.NetworkManager.MultitargetNetworkManager.instance().getCertificate(origin); |
| if (names.length > 0) { |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.showCertificateViewer(names); |
| } |
| }, {className: 'origin-button', jslogContext: 'security.view-certificate-for-origin', title: text}); |
| UI.ARIAUtils.markAsButton(certificateButton); |
| return certificateButton; |
| } |
| |
| static createCertificateViewerButtonForCert(text: string, names: string[]): Element { |
| const certificateButton = UI.UIUtils.createTextButton(text, e => { |
| e.consume(); |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.showCertificateViewer(names); |
| }, {className: 'origin-button', jslogContext: 'security.view-certificate'}); |
| UI.ARIAUtils.markAsButton(certificateButton); |
| return certificateButton; |
| } |
| |
| update(): void { |
| this.view({panel: this}, this, this.contentElement); |
| } |
| |
| private updateVisibleSecurityState(visibleSecurityState: PageVisibleSecurityState): void { |
| this.sidebar.securityOverviewElement.setSecurityState(visibleSecurityState.securityState); |
| this.mainView.updateVisibleSecurityState(visibleSecurityState); |
| } |
| |
| private onVisibleSecurityStateChanged({data}: Common.EventTarget.EventTargetEvent<PageVisibleSecurityState>): void { |
| this.updateVisibleSecurityState(data); |
| } |
| |
| showOrigin(origin: Platform.DevToolsPath.UrlString): void { |
| const originState = this.origins.get(origin); |
| if (!originState) { |
| return; |
| } |
| if (!originState.originView) { |
| originState.originView = new SecurityOriginView(origin, originState); |
| } |
| |
| this.setVisibleView(originState.originView); |
| } |
| |
| override wasShown(): void { |
| super.wasShown(); |
| if (!this.visibleView) { |
| this.sidebar.showLastSelectedElement(); |
| } |
| } |
| |
| override focus(): void { |
| this.sidebar.focus(); |
| } |
| |
| setVisibleView(view: UI.Widget.VBox): void { |
| if (this.visibleView === view) { |
| return; |
| } |
| |
| if (this.visibleView) { |
| this.visibleView.detach(); |
| } |
| |
| this.visibleView = view; |
| |
| if (view) { |
| this.splitWidget.setMainWidget(view); |
| } |
| } |
| |
| private onResponseReceived(event: Common.EventTarget.EventTargetEvent<SDK.NetworkManager.ResponseReceivedEvent>): |
| void { |
| const request = event.data.request; |
| if (request.resourceType() === Common.ResourceType.resourceTypes.Document && request.loaderId) { |
| this.lastResponseReceivedForLoaderId.set(request.loaderId, request); |
| } |
| } |
| |
| private processRequest(request: SDK.NetworkRequest.NetworkRequest): void { |
| const origin = Common.ParsedURL.ParsedURL.extractOrigin(request.url()); |
| |
| if (!origin) { |
| // We don't handle resources like data: URIs. Most of them don't affect the lock icon. |
| return; |
| } |
| |
| let securityState = request.securityState(); |
| if (request.mixedContentType === Protocol.Security.MixedContentType.Blockable || |
| request.mixedContentType === Protocol.Security.MixedContentType.OptionallyBlockable) { |
| securityState = Protocol.Security.SecurityState.Insecure; |
| } |
| |
| const originState = this.origins.get(origin); |
| if (originState) { |
| if (securityStateCompare(securityState, originState.securityState) < 0) { |
| originState.securityState = securityState; |
| const securityDetails = request.securityDetails(); |
| if (securityDetails) { |
| originState.securityDetails = securityDetails; |
| } |
| this.sidebar.updateOrigin(origin, securityState); |
| if (originState.originView) { |
| originState.originView.setSecurityState(securityState); |
| } |
| } |
| } else { |
| // This stores the first security details we see for an origin, but we should |
| // eventually store a (deduplicated) list of all the different security |
| // details we have seen. https://crbug.com/503170 |
| const newOriginState: OriginState = { |
| securityState, |
| securityDetails: request.securityDetails(), |
| loadedFromCache: request.cached(), |
| originView: undefined, |
| }; |
| this.origins.set(origin, newOriginState); |
| |
| this.sidebar.addOrigin(origin, securityState); |
| |
| // Don't construct the origin view yet (let it happen lazily). |
| } |
| } |
| |
| private onRequestFinished(event: Common.EventTarget.EventTargetEvent<SDK.NetworkRequest.NetworkRequest>): void { |
| const request = event.data; |
| this.updateFilterRequestCounts(request); |
| this.processRequest(request); |
| } |
| |
| private updateFilterRequestCounts(request: SDK.NetworkRequest.NetworkRequest): void { |
| if (request.mixedContentType === Protocol.Security.MixedContentType.None) { |
| return; |
| } |
| |
| let filterKey: string = NetworkForward.UIFilter.MixedContentFilterValues.ALL; |
| if (request.wasBlocked()) { |
| filterKey = NetworkForward.UIFilter.MixedContentFilterValues.BLOCKED; |
| } else if (request.mixedContentType === Protocol.Security.MixedContentType.Blockable) { |
| filterKey = NetworkForward.UIFilter.MixedContentFilterValues.BLOCK_OVERRIDDEN; |
| } else if (request.mixedContentType === Protocol.Security.MixedContentType.OptionallyBlockable) { |
| filterKey = NetworkForward.UIFilter.MixedContentFilterValues.DISPLAYED; |
| } |
| |
| const currentCount = this.filterRequestCounts.get(filterKey); |
| if (!currentCount) { |
| this.filterRequestCounts.set(filterKey, 1); |
| } else { |
| this.filterRequestCounts.set(filterKey, currentCount + 1); |
| } |
| |
| this.mainView.refreshExplanations(); |
| } |
| |
| filterRequestCount(filterKey: string): number { |
| return this.filterRequestCounts.get(filterKey) || 0; |
| } |
| |
| modelAdded(securityModel: SecurityModel): void { |
| if (securityModel.target() !== securityModel.target().outermostTarget()) { |
| return; |
| } |
| |
| this.securityModel = securityModel; |
| const resourceTreeModel = securityModel.resourceTreeModel(); |
| const networkManager = securityModel.networkManager(); |
| if (this.eventListeners.length) { |
| Common.EventTarget.removeEventListeners(this.eventListeners); |
| } |
| this.eventListeners = [ |
| securityModel.addEventListener(Events.VisibleSecurityStateChanged, this.onVisibleSecurityStateChanged, this), |
| resourceTreeModel.addEventListener( |
| SDK.ResourceTreeModel.Events.InterstitialShown, this.onInterstitialShown, this), |
| resourceTreeModel.addEventListener( |
| SDK.ResourceTreeModel.Events.InterstitialHidden, this.onInterstitialHidden, this), |
| networkManager.addEventListener(SDK.NetworkManager.Events.ResponseReceived, this.onResponseReceived, this), |
| networkManager.addEventListener(SDK.NetworkManager.Events.RequestFinished, this.onRequestFinished, this), |
| ]; |
| |
| if (resourceTreeModel.isInterstitialShowing) { |
| this.onInterstitialShown(); |
| } |
| } |
| |
| modelRemoved(securityModel: SecurityModel): void { |
| if (this.securityModel !== securityModel) { |
| return; |
| } |
| |
| this.securityModel = null; |
| Common.EventTarget.removeEventListeners(this.eventListeners); |
| } |
| |
| private onPrimaryPageChanged( |
| event: Common.EventTarget.EventTargetEvent< |
| {frame: SDK.ResourceTreeModel.ResourceTreeFrame, type: SDK.ResourceTreeModel.PrimaryPageChangeType}>): void { |
| const {frame} = event.data; |
| const request = this.lastResponseReceivedForLoaderId.get(frame.loaderId); |
| |
| this.sidebar.showLastSelectedElement(); |
| this.sidebar.clearOrigins(); |
| this.origins.clear(); |
| this.lastResponseReceivedForLoaderId.clear(); |
| this.filterRequestCounts.clear(); |
| // After clearing the filtered request counts, refresh the |
| // explanations to reflect the new counts. |
| this.mainView.refreshExplanations(); |
| |
| // If we could not find a matching request (as in the case of clicking |
| // through an interstitial, see https://crbug.com/669309), set the origin |
| // based upon the url data from the PrimaryPageChanged event itself. |
| const origin = Common.ParsedURL.ParsedURL.extractOrigin(request ? request.url() : frame.url); |
| this.sidebar.setMainOrigin(origin); |
| |
| if (request) { |
| this.processRequest(request); |
| } |
| } |
| |
| private onInterstitialShown(): void { |
| // The panel might have been displaying the origin view on the |
| // previously loaded page. When showing an interstitial, switch |
| // back to the sidebar's last shown view. |
| this.sidebar.showLastSelectedElement(); |
| this.sidebar.toggleOriginsList(true /* hidden */); |
| } |
| |
| private onInterstitialHidden(): void { |
| this.sidebar.toggleOriginsList(false /* hidden */); |
| } |
| } |
| |
| export enum OriginGroup { |
| /* eslint-disable @typescript-eslint/naming-convention -- Used by web_tests. */ |
| MainOrigin = 'MainOrigin', |
| NonSecure = 'NonSecure', |
| Secure = 'Secure', |
| Unknown = 'Unknown', |
| /* eslint-enable @typescript-eslint/naming-convention */ |
| } |
| |
| export class SecurityMainView extends UI.Widget.VBox { |
| panel!: SecurityPanel; |
| private readonly summarySection: HTMLElement; |
| private readonly securityExplanationsMain: HTMLElement; |
| private readonly securityExplanationsExtra: HTMLElement; |
| private readonly lockSpectrum: Map<Protocol.Security.SecurityState, HTMLElement>; |
| private summaryText: HTMLElement; |
| private explanations: Array<Protocol.Security.SecurityStateExplanation|SecurityStyleExplanation>|null; |
| private securityState: Protocol.Security.SecurityState|null; |
| constructor(element?: HTMLElement) { |
| super(element, {jslog: `${VisualLogging.pane('security.main-view')}`}); |
| this.registerRequiredCSS(lockIconStyles, mainViewStyles); |
| |
| this.setMinimumSize(200, 100); |
| |
| this.contentElement.classList.add('security-main-view'); |
| |
| this.summarySection = this.contentElement.createChild('div', 'security-summary'); |
| |
| // Info explanations should appear after all others. |
| this.securityExplanationsMain = |
| this.contentElement.createChild('div', 'security-explanation-list security-explanations-main'); |
| this.securityExplanationsExtra = |
| this.contentElement.createChild('div', 'security-explanation-list security-explanations-extra'); |
| |
| // Fill the security summary section. |
| const summaryDiv = this.summarySection.createChild('div', 'security-summary-section-title'); |
| summaryDiv.textContent = i18nString(UIStrings.securityOverview); |
| UI.ARIAUtils.markAsHeading(summaryDiv, 1); |
| |
| const lockSpectrum = this.summarySection.createChild('div', 'lock-spectrum'); |
| this.lockSpectrum = new Map([ |
| [ |
| Protocol.Security.SecurityState.Secure, |
| lockSpectrum.appendChild( |
| getSecurityStateIconForOverview(Protocol.Security.SecurityState.Secure, 'lock-icon lock-icon-secure')), |
| ], |
| [ |
| Protocol.Security.SecurityState.Neutral, |
| lockSpectrum.appendChild( |
| getSecurityStateIconForOverview(Protocol.Security.SecurityState.Neutral, 'lock-icon lock-icon-neutral')), |
| ], |
| [ |
| Protocol.Security.SecurityState.Insecure, |
| lockSpectrum.appendChild( |
| getSecurityStateIconForOverview(Protocol.Security.SecurityState.Insecure, 'lock-icon lock-icon-insecure')), |
| ], |
| ]); |
| UI.Tooltip.Tooltip.install( |
| this.getLockSpectrumDiv(Protocol.Security.SecurityState.Secure), i18nString(UIStrings.secure)); |
| UI.Tooltip.Tooltip.install( |
| this.getLockSpectrumDiv(Protocol.Security.SecurityState.Neutral), i18nString(UIStrings.info)); |
| UI.Tooltip.Tooltip.install( |
| this.getLockSpectrumDiv(Protocol.Security.SecurityState.Insecure), i18nString(UIStrings.notSecure)); |
| |
| this.summarySection.createChild('div', 'triangle-pointer-container') |
| .createChild('div', 'triangle-pointer-wrapper') |
| .createChild('div', 'triangle-pointer'); |
| |
| this.summaryText = this.summarySection.createChild('div', 'security-summary-text'); |
| UI.ARIAUtils.markAsHeading(this.summaryText, 2); |
| |
| this.explanations = null; |
| this.securityState = null; |
| } |
| |
| getLockSpectrumDiv(securityState: Protocol.Security.SecurityState): HTMLElement { |
| const element = this.lockSpectrum.get(securityState); |
| if (!element) { |
| throw new Error(`Invalid argument: ${securityState}`); |
| } |
| return element; |
| } |
| |
| private addExplanation( |
| parent: Element, explanation: Protocol.Security.SecurityStateExplanation|SecurityStyleExplanation): Element { |
| const explanationSection = parent.createChild('div', 'security-explanation'); |
| explanationSection.classList.add('security-explanation-' + explanation.securityState); |
| |
| const icon = getSecurityStateIconForDetailedView( |
| explanation.securityState, 'security-property security-property-' + explanation.securityState); |
| explanationSection.appendChild(icon); |
| const text = explanationSection.createChild('div', 'security-explanation-text'); |
| |
| const explanationHeader = text.createChild('div', 'security-explanation-title'); |
| |
| if (explanation.title) { |
| explanationHeader.createChild('span').textContent = explanation.title + ' - '; |
| explanationHeader.createChild('span', 'security-explanation-title-' + explanation.securityState).textContent = |
| explanation.summary; |
| } else { |
| explanationHeader.textContent = explanation.summary; |
| } |
| |
| text.createChild('div').textContent = explanation.description; |
| |
| if (explanation.certificate.length) { |
| text.appendChild(SecurityPanel.createCertificateViewerButtonForCert( |
| i18nString(UIStrings.viewCertificate), explanation.certificate)); |
| } |
| |
| if (explanation.recommendations?.length) { |
| const recommendationList = text.createChild('ul', 'security-explanation-recommendations'); |
| for (const recommendation of explanation.recommendations) { |
| recommendationList.createChild('li').textContent = recommendation; |
| } |
| } |
| return text; |
| } |
| |
| updateVisibleSecurityState(visibleSecurityState: PageVisibleSecurityState): void { |
| // Remove old state. |
| // It's safe to call this even when this.securityState is undefined. |
| this.summarySection.classList.remove('security-summary-' + this.securityState); |
| |
| // Add new state. |
| this.securityState = visibleSecurityState.securityState; |
| this.summarySection.classList.add('security-summary-' + this.securityState); |
| |
| // Update the color and title of the triangle icon in the lock spectrum to |
| // match the security state. |
| if (this.securityState === Protocol.Security.SecurityState.Insecure) { |
| this.getLockSpectrumDiv(Protocol.Security.SecurityState.Insecure).classList.add('lock-icon-insecure'); |
| this.getLockSpectrumDiv(Protocol.Security.SecurityState.Insecure).classList.remove('lock-icon-insecure-broken'); |
| UI.Tooltip.Tooltip.install( |
| this.getLockSpectrumDiv(Protocol.Security.SecurityState.Insecure), i18nString(UIStrings.notSecure)); |
| } else if (this.securityState === Protocol.Security.SecurityState.InsecureBroken) { |
| this.getLockSpectrumDiv(Protocol.Security.SecurityState.Insecure).classList.add('lock-icon-insecure-broken'); |
| this.getLockSpectrumDiv(Protocol.Security.SecurityState.Insecure).classList.remove('lock-icon-insecure'); |
| UI.Tooltip.Tooltip.install( |
| this.getLockSpectrumDiv(Protocol.Security.SecurityState.Insecure), i18nString(UIStrings.notSecureBroken)); |
| } |
| |
| const {summary, explanations} = this.getSecuritySummaryAndExplanations(visibleSecurityState); |
| // Use override summary if present, otherwise use base explanation |
| this.summaryText.textContent = summary || SummaryMessages[this.securityState](); |
| |
| this.explanations = this.orderExplanations(explanations); |
| |
| this.refreshExplanations(); |
| } |
| |
| private getSecuritySummaryAndExplanations(visibleSecurityState: PageVisibleSecurityState): |
| {summary: (string|undefined), explanations: SecurityStyleExplanation[]} { |
| const {securityState, securityStateIssueIds} = visibleSecurityState; |
| let summary; |
| const explanations: SecurityStyleExplanation[] = []; |
| summary = this.explainSafetyTipSecurity(visibleSecurityState, summary, explanations); |
| if (securityStateIssueIds.includes('malicious-content')) { |
| summary = i18nString(UIStrings.thisPageIsDangerousFlaggedBy); |
| // Always insert SafeBrowsing explanation at the front. |
| explanations.unshift(new SecurityStyleExplanation( |
| Protocol.Security.SecurityState.Insecure, undefined, i18nString(UIStrings.flaggedByGoogleSafeBrowsing), |
| i18nString(UIStrings.toCheckThisPagesStatusVisit))); |
| } else if ( |
| securityStateIssueIds.includes('is-error-page') && |
| (visibleSecurityState.certificateSecurityState?.certificateNetworkError === null)) { |
| summary = i18nString(UIStrings.thisIsAnErrorPage); |
| // In the case of a non cert error page, we usually don't have a |
| // certificate, connection, or content that needs to be explained, e.g. in |
| // the case of a net error, so we can early return. |
| return {summary, explanations}; |
| } else if ( |
| securityState === Protocol.Security.SecurityState.InsecureBroken && |
| securityStateIssueIds.includes('scheme-is-not-cryptographic')) { |
| summary = summary || i18nString(UIStrings.thisPageIsInsecureUnencrypted); |
| } |
| |
| if (securityStateIssueIds.includes('scheme-is-not-cryptographic')) { |
| if (securityState === Protocol.Security.SecurityState.Neutral && |
| !securityStateIssueIds.includes('insecure-origin')) { |
| summary = i18nString(UIStrings.thisPageHasANonhttpsSecureOrigin); |
| } |
| return {summary, explanations}; |
| } |
| |
| this.explainCertificateSecurity(visibleSecurityState, explanations); |
| this.explainConnectionSecurity(visibleSecurityState, explanations); |
| this.explainContentSecurity(visibleSecurityState, explanations); |
| return {summary, explanations}; |
| } |
| |
| private explainSafetyTipSecurity( |
| visibleSecurityState: PageVisibleSecurityState, summary: string|undefined, |
| explanations: SecurityStyleExplanation[]): string|undefined { |
| const {securityStateIssueIds, safetyTipInfo} = visibleSecurityState; |
| const currentExplanations = []; |
| |
| if (securityStateIssueIds.includes('bad_reputation')) { |
| const formatedDescription = `${i18nString(UIStrings.chromeHasDeterminedThatThisSiteS)}\n\n${ |
| i18nString(UIStrings.ifYouBelieveThisIsShownIn)}`; |
| currentExplanations.push({ |
| summary: i18nString(UIStrings.thisPageIsSuspicious), |
| description: formatedDescription, |
| }); |
| } else if (securityStateIssueIds.includes('lookalike') && safetyTipInfo?.safeUrl) { |
| const hostname = new URL(safetyTipInfo.safeUrl).hostname; |
| const hostnamePlaceholder = {PH1: hostname}; |
| const formattedDescriptionSafety = |
| `${i18nString(UIStrings.thisSitesHostnameLooksSimilarToP, hostnamePlaceholder)}\n\n${ |
| i18nString(UIStrings.ifYouBelieveThisIsShownInErrorSafety)}`; |
| currentExplanations.push( |
| {summary: i18nString(UIStrings.possibleSpoofingUrl), description: formattedDescriptionSafety}); |
| } |
| |
| if (currentExplanations.length > 0) { |
| // To avoid overwriting SafeBrowsing's title, set the main summary only if |
| // it's empty. The title set here can be overridden by later checks (e.g. |
| // bad HTTP). |
| summary = summary || i18nString(UIStrings.thisPageIsSuspiciousFlaggedBy); |
| explanations.push(new SecurityStyleExplanation( |
| Protocol.Security.SecurityState.Insecure, undefined, currentExplanations[0].summary, |
| currentExplanations[0].description)); |
| } |
| return summary; |
| } |
| |
| private explainCertificateSecurity( |
| visibleSecurityState: PageVisibleSecurityState, explanations: SecurityStyleExplanation[]): void { |
| const {certificateSecurityState, securityStateIssueIds} = visibleSecurityState; |
| const title = i18nString(UIStrings.certificate); |
| if (certificateSecurityState?.certificateHasSha1Signature) { |
| const explanationSummary = i18nString(UIStrings.insecureSha); |
| const description = i18nString(UIStrings.theCertificateChainForThisSite); |
| if (certificateSecurityState.certificateHasWeakSignature) { |
| explanations.push(new SecurityStyleExplanation( |
| Protocol.Security.SecurityState.Insecure, title, explanationSummary, description, |
| certificateSecurityState.certificate, Protocol.Security.MixedContentType.None)); |
| } else { |
| explanations.push(new SecurityStyleExplanation( |
| Protocol.Security.SecurityState.Neutral, title, explanationSummary, description, |
| certificateSecurityState.certificate, Protocol.Security.MixedContentType.None)); |
| } |
| } |
| |
| if (certificateSecurityState && securityStateIssueIds.includes('cert-missing-subject-alt-name')) { |
| explanations.push(new SecurityStyleExplanation( |
| Protocol.Security.SecurityState.Insecure, title, i18nString(UIStrings.subjectAlternativeNameMissing), |
| i18nString(UIStrings.theCertificateForThisSiteDoesNot), certificateSecurityState.certificate, |
| Protocol.Security.MixedContentType.None)); |
| } |
| |
| if (certificateSecurityState && certificateSecurityState.certificateNetworkError !== null) { |
| explanations.push(new SecurityStyleExplanation( |
| Protocol.Security.SecurityState.Insecure, title, i18nString(UIStrings.missing), |
| i18nString(UIStrings.thisSiteIsMissingAValidTrusted, {PH1: certificateSecurityState.certificateNetworkError}), |
| certificateSecurityState.certificate, Protocol.Security.MixedContentType.None)); |
| } else if (certificateSecurityState && !certificateSecurityState.certificateHasSha1Signature) { |
| explanations.push(new SecurityStyleExplanation( |
| Protocol.Security.SecurityState.Secure, title, i18nString(UIStrings.validAndTrusted), |
| i18nString(UIStrings.theConnectionToThisSiteIsUsingA, {PH1: certificateSecurityState.issuer}), |
| certificateSecurityState.certificate, Protocol.Security.MixedContentType.None)); |
| } |
| |
| if (securityStateIssueIds.includes('pkp-bypassed')) { |
| explanations.push(new SecurityStyleExplanation( |
| Protocol.Security.SecurityState.Info, title, i18nString(UIStrings.publickeypinningBypassed), |
| i18nString(UIStrings.publickeypinningWasBypassedByA))); |
| } |
| |
| if (certificateSecurityState?.isCertificateExpiringSoon()) { |
| explanations.push(new SecurityStyleExplanation( |
| Protocol.Security.SecurityState.Info, undefined, i18nString(UIStrings.certificateExpiresSoon), |
| i18nString(UIStrings.theCertificateForThisSiteExpires))); |
| } |
| } |
| |
| private explainConnectionSecurity( |
| visibleSecurityState: PageVisibleSecurityState, explanations: SecurityStyleExplanation[]): void { |
| const certificateSecurityState = visibleSecurityState.certificateSecurityState; |
| if (!certificateSecurityState) { |
| return; |
| } |
| |
| const title = i18nString(UIStrings.connection); |
| if (certificateSecurityState.modernSSL) { |
| explanations.push(new SecurityStyleExplanation( |
| Protocol.Security.SecurityState.Secure, title, i18nString(UIStrings.secureConnectionSettings), |
| i18nString(UIStrings.theConnectionToThisSiteIs, { |
| PH1: certificateSecurityState.protocol, |
| PH2: certificateSecurityState.getKeyExchangeName(), |
| PH3: certificateSecurityState.getCipherFullName(), |
| }))); |
| return; |
| } |
| |
| const recommendations = []; |
| if (certificateSecurityState.obsoleteSslProtocol) { |
| recommendations.push(i18nString(UIStrings.sIsObsoleteEnableTlsOrLater, {PH1: certificateSecurityState.protocol})); |
| } |
| if (certificateSecurityState.obsoleteSslKeyExchange) { |
| recommendations.push(i18nString(UIStrings.rsaKeyExchangeIsObsoleteEnableAn)); |
| } |
| if (certificateSecurityState.obsoleteSslCipher) { |
| recommendations.push( |
| i18nString(UIStrings.sIsObsoleteEnableAnAesgcmbased, {PH1: certificateSecurityState.cipher})); |
| } |
| if (certificateSecurityState.obsoleteSslSignature) { |
| recommendations.push(i18nString(UIStrings.theServerSignatureUsesShaWhichIs)); |
| } |
| |
| explanations.push(new SecurityStyleExplanation( |
| Protocol.Security.SecurityState.Info, title, i18nString(UIStrings.obsoleteConnectionSettings), |
| i18nString(UIStrings.theConnectionToThisSiteIs, { |
| PH1: certificateSecurityState.protocol, |
| PH2: certificateSecurityState.getKeyExchangeName(), |
| PH3: certificateSecurityState.getCipherFullName(), |
| }), |
| undefined, undefined, recommendations)); |
| } |
| |
| private explainContentSecurity( |
| visibleSecurityState: PageVisibleSecurityState, explanations: SecurityStyleExplanation[]): void { |
| // Add the secure explanation unless there is an issue. |
| let addSecureExplanation = true; |
| const title = i18nString(UIStrings.resources); |
| const securityStateIssueIds = visibleSecurityState.securityStateIssueIds; |
| |
| if (securityStateIssueIds.includes('ran-mixed-content')) { |
| addSecureExplanation = false; |
| explanations.push(new SecurityStyleExplanation( |
| Protocol.Security.SecurityState.Insecure, title, i18nString(UIStrings.activeMixedContent), |
| i18nString(UIStrings.youHaveRecentlyAllowedNonsecure), [], Protocol.Security.MixedContentType.Blockable)); |
| } |
| |
| if (securityStateIssueIds.includes('displayed-mixed-content')) { |
| addSecureExplanation = false; |
| explanations.push(new SecurityStyleExplanation( |
| Protocol.Security.SecurityState.Neutral, title, i18nString(UIStrings.mixedContent), |
| i18nString(UIStrings.thisPageIncludesHttpResources), [], |
| Protocol.Security.MixedContentType.OptionallyBlockable)); |
| } |
| |
| if (securityStateIssueIds.includes('contained-mixed-form')) { |
| addSecureExplanation = false; |
| explanations.push(new SecurityStyleExplanation( |
| Protocol.Security.SecurityState.Neutral, title, i18nString(UIStrings.nonsecureForm), |
| i18nString(UIStrings.thisPageIncludesAFormWithA))); |
| } |
| |
| if (visibleSecurityState.certificateSecurityState?.certificateNetworkError === null) { |
| if (securityStateIssueIds.includes('ran-content-with-cert-error')) { |
| addSecureExplanation = false; |
| explanations.push(new SecurityStyleExplanation( |
| Protocol.Security.SecurityState.Insecure, title, i18nString(UIStrings.activeContentWithCertificate), |
| i18nString(UIStrings.youHaveRecentlyAllowedContent))); |
| } |
| |
| if (securityStateIssueIds.includes('displayed-content-with-cert-errors')) { |
| addSecureExplanation = false; |
| explanations.push(new SecurityStyleExplanation( |
| Protocol.Security.SecurityState.Neutral, title, i18nString(UIStrings.contentWithCertificateErrors), |
| i18nString(UIStrings.thisPageIncludesResourcesThat))); |
| } |
| } |
| |
| if (addSecureExplanation) { |
| if (!securityStateIssueIds.includes('scheme-is-not-cryptographic')) { |
| explanations.push(new SecurityStyleExplanation( |
| Protocol.Security.SecurityState.Secure, title, i18nString(UIStrings.allServedSecurely), |
| i18nString(UIStrings.allResourcesOnThisPageAreServed))); |
| } |
| } |
| } |
| |
| private orderExplanations(explanations: SecurityStyleExplanation[]): SecurityStyleExplanation[] { |
| if (explanations.length === 0) { |
| return explanations; |
| } |
| const securityStateOrder = [ |
| Protocol.Security.SecurityState.Insecure, |
| Protocol.Security.SecurityState.Neutral, |
| Protocol.Security.SecurityState.Secure, |
| Protocol.Security.SecurityState.Info, |
| ]; |
| const orderedExplanations = []; |
| for (const securityState of securityStateOrder) { |
| orderedExplanations.push(...explanations.filter(explanation => explanation.securityState === securityState)); |
| } |
| return orderedExplanations; |
| } |
| |
| refreshExplanations(): void { |
| this.securityExplanationsMain.removeChildren(); |
| this.securityExplanationsExtra.removeChildren(); |
| if (!this.explanations) { |
| return; |
| } |
| for (const explanation of this.explanations) { |
| if (explanation.securityState === Protocol.Security.SecurityState.Info) { |
| this.addExplanation(this.securityExplanationsExtra, explanation); |
| } else { |
| switch (explanation.mixedContentType) { |
| case Protocol.Security.MixedContentType.Blockable: |
| this.addMixedContentExplanation( |
| this.securityExplanationsMain, explanation, |
| NetworkForward.UIFilter.MixedContentFilterValues.BLOCK_OVERRIDDEN); |
| break; |
| case Protocol.Security.MixedContentType.OptionallyBlockable: |
| this.addMixedContentExplanation( |
| this.securityExplanationsMain, explanation, NetworkForward.UIFilter.MixedContentFilterValues.DISPLAYED); |
| break; |
| default: |
| this.addExplanation(this.securityExplanationsMain, explanation); |
| break; |
| } |
| } |
| } |
| |
| if (this.panel.filterRequestCount(NetworkForward.UIFilter.MixedContentFilterValues.BLOCKED) > 0) { |
| const explanation = { |
| securityState: Protocol.Security.SecurityState.Info, |
| summary: i18nString(UIStrings.blockedMixedContent), |
| description: i18nString(UIStrings.yourPageRequestedNonsecure), |
| mixedContentType: Protocol.Security.MixedContentType.Blockable, |
| certificate: [], |
| title: '', |
| } as Protocol.Security.SecurityStateExplanation; |
| this.addMixedContentExplanation( |
| this.securityExplanationsMain, explanation, NetworkForward.UIFilter.MixedContentFilterValues.BLOCKED); |
| } |
| } |
| |
| private addMixedContentExplanation( |
| parent: Element, explanation: Protocol.Security.SecurityStateExplanation|SecurityStyleExplanation, |
| filterKey: string): void { |
| const element = this.addExplanation(parent, explanation); |
| |
| const filterRequestCount = this.panel.filterRequestCount(filterKey); |
| if (!filterRequestCount) { |
| // Network instrumentation might not have been enabled for the page |
| // load, so the security panel does not necessarily know a count of |
| // individual mixed requests at this point. Prompt them to refresh |
| // instead of pointing them to the Network panel to get prompted |
| // to refresh. |
| const refreshPrompt = element.createChild('div', 'security-mixed-content'); |
| refreshPrompt.textContent = i18nString(UIStrings.reloadThePageToRecordRequestsFor); |
| return; |
| } |
| |
| const requestsAnchor = element.createChild('button', 'security-mixed-content devtools-link text-button link-style'); |
| UI.ARIAUtils.markAsLink(requestsAnchor); |
| requestsAnchor.tabIndex = 0; |
| requestsAnchor.textContent = i18nString(UIStrings.viewDRequestsInNetworkPanel, {n: filterRequestCount}); |
| |
| requestsAnchor.addEventListener('click', this.showNetworkFilter.bind(this, filterKey)); |
| } |
| |
| showNetworkFilter(filterKey: string, e: Event): void { |
| e.consume(); |
| void Common.Revealer.reveal(NetworkForward.UIFilter.UIRequestFilter.filters( |
| [{filterType: NetworkForward.UIFilter.FilterType.MixedContent, filterValue: filterKey}])); |
| } |
| } |
| |
| export class SecurityOriginView extends UI.Widget.VBox { |
| private readonly originLockIcon: HTMLElement; |
| constructor(origin: Platform.DevToolsPath.UrlString, originState: OriginState) { |
| super({jslog: `${VisualLogging.pane('security.origin-view')}`}); |
| this.registerRequiredCSS(originViewStyles, lockIconStyles); |
| this.setMinimumSize(200, 100); |
| |
| this.element.classList.add('security-origin-view'); |
| |
| const titleSection = this.element.createChild('div', 'title-section'); |
| const titleDiv = titleSection.createChild('div', 'title-section-header'); |
| titleDiv.textContent = i18nString(UIStrings.origin); |
| UI.ARIAUtils.markAsHeading(titleDiv, 1); |
| |
| const originDisplay = titleSection.createChild('div', 'origin-display'); |
| this.originLockIcon = originDisplay.createChild('span'); |
| const icon = getSecurityStateIconForDetailedView( |
| originState.securityState, `security-property security-property-${originState.securityState}`); |
| this.originLockIcon.appendChild(icon); |
| |
| originDisplay.appendChild(createHighlightedUrl(origin, originState.securityState)); |
| |
| const originNetworkDiv = titleSection.createChild('div', 'view-network-button'); |
| const originNetworkButton = UI.UIUtils.createTextButton(i18nString(UIStrings.viewRequestsInNetworkPanel), event => { |
| event.consume(); |
| const parsedURL = new Common.ParsedURL.ParsedURL(origin); |
| void Common.Revealer.reveal(NetworkForward.UIFilter.UIRequestFilter.filters([ |
| {filterType: NetworkForward.UIFilter.FilterType.Domain, filterValue: parsedURL.host}, |
| {filterType: NetworkForward.UIFilter.FilterType.Scheme, filterValue: parsedURL.scheme}, |
| ])); |
| }, {jslogContext: 'reveal-in-network'}); |
| originNetworkDiv.appendChild(originNetworkButton); |
| UI.ARIAUtils.markAsLink(originNetworkButton); |
| |
| if (originState.securityDetails) { |
| const connectionSection = this.element.createChild('div', 'origin-view-section'); |
| const connectionDiv = connectionSection.createChild('div', 'origin-view-section-title'); |
| connectionDiv.textContent = i18nString(UIStrings.connection); |
| UI.ARIAUtils.markAsHeading(connectionDiv, 2); |
| |
| let table: SecurityDetailsTable = new SecurityDetailsTable(); |
| connectionSection.appendChild(table.element()); |
| table.addRow(i18nString(UIStrings.protocol), originState.securityDetails.protocol); |
| |
| // A TLS connection negotiates a cipher suite and, when doing an ephemeral |
| // ECDH key exchange, a "named group". In TLS 1.2, the cipher suite is |
| // named like TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256. The DevTools protocol |
| // tried to decompose this name and calls the "ECDHE_RSA" portion the |
| // "keyExchange", because it determined the rough shape of the key |
| // exchange portion of the handshake. (A keyExchange of "RSA" meant a very |
| // different handshake set.) But ECDHE_RSA was still parameterized by a |
| // named group (e.g. X25519), which the DevTools protocol exposes as |
| // "keyExchangeGroup". |
| // |
| // Then, starting TLS 1.3, the cipher suites are named like |
| // TLS_AES_128_GCM_SHA256. The handshake shape is implicit in the |
| // protocol. keyExchange is empty and we only have keyExchangeGroup. |
| // |
| // "Key exchange group" isn't common terminology and, in TLS 1.3, |
| // something like "X25519" is better labelled as "key exchange" than "key |
| // exchange group" anyway. So combine the two fields when displaying in |
| // the UI. |
| if (originState.securityDetails.keyExchange && originState.securityDetails.keyExchangeGroup) { |
| table.addRow( |
| i18nString(UIStrings.keyExchange), |
| originState.securityDetails.keyExchange + ' with ' + originState.securityDetails.keyExchangeGroup); |
| } else if (originState.securityDetails.keyExchange) { |
| table.addRow(i18nString(UIStrings.keyExchange), originState.securityDetails.keyExchange); |
| } else if (originState.securityDetails.keyExchangeGroup) { |
| table.addRow(i18nString(UIStrings.keyExchange), originState.securityDetails.keyExchangeGroup); |
| } |
| |
| if (originState.securityDetails.serverSignatureAlgorithm) { |
| // See https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-signaturescheme |
| let sigString = SignatureSchemeStrings.get(originState.securityDetails.serverSignatureAlgorithm); |
| sigString ??= |
| i18nString(UIStrings.unknownField) + ' (' + originState.securityDetails.serverSignatureAlgorithm + ')'; |
| table.addRow(i18nString(UIStrings.serverSignature), sigString); |
| } |
| |
| table.addRow( |
| i18nString(UIStrings.cipher), |
| originState.securityDetails.cipher + |
| (originState.securityDetails.mac ? ' with ' + originState.securityDetails.mac : '')); |
| |
| if (originState.securityDetails.encryptedClientHello) { |
| table.addRow(i18nString(UIStrings.encryptedClientHello), i18nString(UIStrings.enabled)); |
| } |
| |
| // Create the certificate section outside the callback, so that it appears in the right place. |
| const certificateSection = this.element.createChild('div', 'origin-view-section'); |
| const certificateDiv = certificateSection.createChild('div', 'origin-view-section-title'); |
| certificateDiv.textContent = i18nString(UIStrings.certificate); |
| UI.ARIAUtils.markAsHeading(certificateDiv, 2); |
| |
| const sctListLength = originState.securityDetails.signedCertificateTimestampList.length; |
| const ctCompliance = originState.securityDetails.certificateTransparencyCompliance; |
| let sctSection; |
| if (sctListLength || ctCompliance !== Protocol.Network.CertificateTransparencyCompliance.Unknown) { |
| // Create the Certificate Transparency section outside the callback, so that it appears in the right place. |
| sctSection = this.element.createChild('div', 'origin-view-section'); |
| const sctDiv = sctSection.createChild('div', 'origin-view-section-title'); |
| sctDiv.textContent = i18nString(UIStrings.certificateTransparency); |
| UI.ARIAUtils.markAsHeading(sctDiv, 2); |
| } |
| |
| const sanDiv = this.createSanDiv(originState.securityDetails.sanList); |
| const validFromString = new Date(1000 * originState.securityDetails.validFrom).toUTCString(); |
| const validUntilString = new Date(1000 * originState.securityDetails.validTo).toUTCString(); |
| |
| table = new SecurityDetailsTable(); |
| certificateSection.appendChild(table.element()); |
| table.addRow(i18nString(UIStrings.subject), originState.securityDetails.subjectName); |
| table.addRow(i18n.i18n.lockedString('SAN'), sanDiv); |
| table.addRow(i18nString(UIStrings.validFrom), validFromString); |
| table.addRow(i18nString(UIStrings.validUntil), validUntilString); |
| table.addRow(i18nString(UIStrings.issuer), originState.securityDetails.issuer); |
| |
| table.addRow( |
| '', |
| SecurityPanel.createCertificateViewerButtonForOrigin( |
| i18nString(UIStrings.openFullCertificateDetails), origin)); |
| |
| if (!sctSection) { |
| return; |
| } |
| |
| // Show summary of SCT(s) of Certificate Transparency. |
| const sctSummaryTable = new SecurityDetailsTable(); |
| sctSummaryTable.element().classList.add('sct-summary'); |
| sctSection.appendChild(sctSummaryTable.element()); |
| for (let i = 0; i < sctListLength; i++) { |
| const sct = originState.securityDetails.signedCertificateTimestampList[i]; |
| sctSummaryTable.addRow( |
| i18nString(UIStrings.sct), sct.logDescription + ' (' + sct.origin + ', ' + sct.status + ')'); |
| } |
| |
| // Show detailed SCT(s) of Certificate Transparency. |
| const sctTableWrapper = sctSection.createChild('div', 'sct-details'); |
| sctTableWrapper.classList.add('hidden'); |
| for (let i = 0; i < sctListLength; i++) { |
| const sctTable = new SecurityDetailsTable(); |
| sctTableWrapper.appendChild(sctTable.element()); |
| const sct = originState.securityDetails.signedCertificateTimestampList[i]; |
| sctTable.addRow(i18nString(UIStrings.logName), sct.logDescription); |
| sctTable.addRow(i18nString(UIStrings.logId), sct.logId.replace(/(.{2})/g, '$1 ')); |
| sctTable.addRow(i18nString(UIStrings.validationStatus), sct.status); |
| sctTable.addRow(i18nString(UIStrings.source), sct.origin); |
| sctTable.addRow(i18nString(UIStrings.issuedAt), new Date(sct.timestamp).toUTCString()); |
| sctTable.addRow(i18nString(UIStrings.hashAlgorithm), sct.hashAlgorithm); |
| sctTable.addRow(i18nString(UIStrings.signatureAlgorithm), sct.signatureAlgorithm); |
| sctTable.addRow(i18nString(UIStrings.signatureData), sct.signatureData.replace(/(.{2})/g, '$1 ')); |
| } |
| |
| // Add link to toggle between displaying of the summary of the SCT(s) and the detailed SCT(s). |
| if (sctListLength) { |
| function toggleSctDetailsDisplay(): void { |
| let buttonText; |
| const isDetailsShown = !sctTableWrapper.classList.contains('hidden'); |
| if (isDetailsShown) { |
| buttonText = i18nString(UIStrings.showFullDetails); |
| } else { |
| buttonText = i18nString(UIStrings.hideFullDetails); |
| } |
| toggleSctsDetailsLink.textContent = buttonText; |
| UI.ARIAUtils.setLabel(toggleSctsDetailsLink, buttonText); |
| UI.ARIAUtils.setExpanded(toggleSctsDetailsLink, !isDetailsShown); |
| sctSummaryTable.element().classList.toggle('hidden'); |
| sctTableWrapper.classList.toggle('hidden'); |
| } |
| const toggleSctsDetailsLink = UI.UIUtils.createTextButton( |
| i18nString(UIStrings.showFullDetails), toggleSctDetailsDisplay, |
| {className: 'details-toggle', jslogContext: 'security.toggle-scts-details'}); |
| sctSection.appendChild(toggleSctsDetailsLink); |
| } |
| |
| switch (ctCompliance) { |
| case Protocol.Network.CertificateTransparencyCompliance.Compliant: |
| sctSection.createChild('div', 'origin-view-section-notes').textContent = |
| i18nString(UIStrings.thisRequestCompliesWithChromes); |
| break; |
| case Protocol.Network.CertificateTransparencyCompliance.NotCompliant: |
| sctSection.createChild('div', 'origin-view-section-notes').textContent = |
| i18nString(UIStrings.thisRequestDoesNotComplyWith); |
| break; |
| case Protocol.Network.CertificateTransparencyCompliance.Unknown: |
| break; |
| } |
| |
| const noteSection = this.element.createChild('div', 'origin-view-section origin-view-notes'); |
| if (originState.loadedFromCache) { |
| noteSection.createChild('div').textContent = i18nString(UIStrings.thisResponseWasLoadedFromCache); |
| } |
| noteSection.createChild('div').textContent = i18nString(UIStrings.theSecurityDetailsAboveAreFrom); |
| } else if (originState.securityState === Protocol.Security.SecurityState.Secure) { |
| // If the security state is secure but there are no security details, |
| // this means that the origin is a non-cryptographic secure origin, e.g. |
| // chrome:// or about:. |
| const secureSection = this.element.createChild('div', 'origin-view-section'); |
| const secureDiv = secureSection.createChild('div', 'origin-view-section-title'); |
| secureDiv.textContent = i18nString(UIStrings.secure); |
| UI.ARIAUtils.markAsHeading(secureDiv, 2); |
| secureSection.createChild('div').textContent = i18nString(UIStrings.thisOriginIsANonhttpsSecure); |
| } else if (originState.securityState !== Protocol.Security.SecurityState.Unknown) { |
| const notSecureSection = this.element.createChild('div', 'origin-view-section'); |
| const notSecureDiv = notSecureSection.createChild('div', 'origin-view-section-title'); |
| notSecureDiv.textContent = i18nString(UIStrings.notSecure); |
| UI.ARIAUtils.markAsHeading(notSecureDiv, 2); |
| notSecureSection.createChild('div').textContent = i18nString(UIStrings.yourConnectionToThisOriginIsNot); |
| } else { |
| const noInfoSection = this.element.createChild('div', 'origin-view-section'); |
| const noInfoDiv = noInfoSection.createChild('div', 'origin-view-section-title'); |
| noInfoDiv.textContent = i18nString(UIStrings.noSecurityInformation); |
| UI.ARIAUtils.markAsHeading(noInfoDiv, 2); |
| noInfoSection.createChild('div').textContent = i18nString(UIStrings.noSecurityDetailsAreAvailableFor); |
| } |
| } |
| |
| private createSanDiv(sanList: string[]): Element { |
| const sanDiv = document.createElement('div'); |
| if (sanList.length === 0) { |
| sanDiv.textContent = i18nString(UIStrings.na); |
| sanDiv.classList.add('empty-san'); |
| } else { |
| const truncatedNumToShow = 2; |
| const listIsTruncated = sanList.length > truncatedNumToShow + 1; |
| for (let i = 0; i < sanList.length; i++) { |
| const span = sanDiv.createChild('span', 'san-entry'); |
| span.textContent = sanList[i]; |
| if (listIsTruncated && i >= truncatedNumToShow) { |
| span.classList.add('truncated-entry'); |
| } |
| } |
| if (listIsTruncated) { |
| function toggleSANTruncation(): void { |
| const isTruncated = sanDiv.classList.contains('truncated-san'); |
| let buttonText; |
| if (isTruncated) { |
| sanDiv.classList.remove('truncated-san'); |
| buttonText = i18nString(UIStrings.showLess); |
| } else { |
| sanDiv.classList.add('truncated-san'); |
| buttonText = i18nString(UIStrings.showMoreSTotal, {PH1: sanList.length}); |
| } |
| truncatedSANToggle.textContent = buttonText; |
| UI.ARIAUtils.setLabel(truncatedSANToggle, buttonText); |
| UI.ARIAUtils.setExpanded(truncatedSANToggle, isTruncated); |
| } |
| const truncatedSANToggle = UI.UIUtils.createTextButton( |
| i18nString(UIStrings.showMoreSTotal, {PH1: sanList.length}), toggleSANTruncation, |
| {jslogContext: 'security.toggle-san-truncation'}); |
| sanDiv.appendChild(truncatedSANToggle); |
| toggleSANTruncation(); |
| } |
| } |
| return sanDiv; |
| } |
| |
| setSecurityState(newSecurityState: Protocol.Security.SecurityState): void { |
| this.originLockIcon.removeChildren(); |
| const icon = getSecurityStateIconForDetailedView( |
| newSecurityState, `security-property security-property-${newSecurityState}`); |
| this.originLockIcon.appendChild(icon); |
| } |
| } |
| |
| export class SecurityDetailsTable { |
| readonly #element: HTMLTableElement; |
| |
| constructor() { |
| this.#element = document.createElement('table'); |
| this.#element.classList.add('details-table'); |
| } |
| |
| element(): HTMLTableElement { |
| return this.#element; |
| } |
| |
| addRow(key: string, value: string|Node): void { |
| const row = this.#element.createChild('tr', 'details-table-row'); |
| row.createChild('td').textContent = key; |
| |
| const valueCell = row.createChild('td'); |
| if (typeof value === 'string') { |
| valueCell.textContent = value; |
| } else { |
| valueCell.appendChild(value); |
| } |
| } |
| } |
| export interface OriginState { |
| securityState: Protocol.Security.SecurityState; |
| securityDetails: Protocol.Network.SecurityDetails|null; |
| loadedFromCache: boolean; |
| originView?: SecurityOriginView|null; |
| } |
| |
| export type Origin = Platform.DevToolsPath.UrlString; |