|
1 | | -import { |
2 | | - X509Certificate, |
3 | | - KeyUsagesExtension, |
4 | | - KeyUsageFlags, |
5 | | -} from "@peculiar/x509"; |
6 | | -import { isAxiosError } from "axios"; |
7 | | -import * as tls from "node:tls"; |
8 | | -import * as vscode from "vscode"; |
9 | | - |
10 | | -import { type Logger } from "../logging/logger"; |
11 | | - |
12 | | -import { toError } from "./errorUtils"; |
13 | | - |
14 | | -// X509_ERR_CODE represents error codes as returned from BoringSSL/OpenSSL. |
15 | | -export enum X509_ERR_CODE { |
16 | | - UNABLE_TO_VERIFY_LEAF_SIGNATURE = "UNABLE_TO_VERIFY_LEAF_SIGNATURE", |
17 | | - DEPTH_ZERO_SELF_SIGNED_CERT = "DEPTH_ZERO_SELF_SIGNED_CERT", |
18 | | - SELF_SIGNED_CERT_IN_CHAIN = "SELF_SIGNED_CERT_IN_CHAIN", |
19 | | -} |
20 | | - |
21 | | -// X509_ERR contains human-friendly versions of TLS errors. |
22 | | -export enum X509_ERR { |
23 | | - PARTIAL_CHAIN = "Your Coder deployment's certificate cannot be verified because a certificate is missing from its chain. To fix this your deployment's administrator must bundle the missing certificates.", |
24 | | - // NON_SIGNING can be removed if BoringSSL is patched and the patch makes it |
25 | | - // into the version of Electron used by VS Code. |
26 | | - NON_SIGNING = "Your Coder deployment's certificate is not marked as being capable of signing. VS Code uses a version of Electron that does not support certificates like this even if they are self-issued. The certificate must be regenerated with the certificate signing capability.", |
27 | | - UNTRUSTED_LEAF = "Your Coder deployment's certificate does not appear to be trusted by this system. The certificate must be added to this system's trust store.", |
28 | | - UNTRUSTED_CHAIN = "Your Coder deployment's certificate chain does not appear to be trusted by this system. The root of the certificate chain must be added to this system's trust store. ", |
29 | | -} |
30 | | - |
31 | | -export class CertificateError extends Error { |
32 | | - public static readonly ActionAllowInsecure = "Allow Insecure"; |
33 | | - public static readonly ActionOK = "OK"; |
34 | | - public static readonly InsecureMessage = |
35 | | - 'The Coder extension will no longer verify TLS on HTTPS requests. You can change this at any time with the "coder.insecure" property in your VS Code settings.'; |
36 | | - |
37 | | - private constructor( |
38 | | - message: string, |
39 | | - public readonly x509Err?: X509_ERR, |
40 | | - ) { |
41 | | - super("Secure connection to your Coder deployment failed: " + message); |
42 | | - } |
43 | | - |
44 | | - // maybeWrap returns a CertificateError if the code is a certificate error |
45 | | - // otherwise it returns the original error. |
46 | | - static async maybeWrap<T>( |
47 | | - err: T, |
48 | | - address: string, |
49 | | - logger: Logger, |
50 | | - ): Promise<CertificateError | T> { |
51 | | - if (isAxiosError(err)) { |
52 | | - switch (err.code) { |
53 | | - case X509_ERR_CODE.UNABLE_TO_VERIFY_LEAF_SIGNATURE: |
54 | | - // "Unable to verify" can mean different things so we will attempt to |
55 | | - // parse the certificate and determine which it is. |
56 | | - try { |
57 | | - const cause = |
58 | | - await CertificateError.determineVerifyErrorCause(address); |
59 | | - return new CertificateError(err.message, cause); |
60 | | - } catch (error) { |
61 | | - logger.warn(`Failed to parse certificate from ${address}`, error); |
62 | | - break; |
63 | | - } |
64 | | - case X509_ERR_CODE.DEPTH_ZERO_SELF_SIGNED_CERT: |
65 | | - return new CertificateError(err.message, X509_ERR.UNTRUSTED_LEAF); |
66 | | - case X509_ERR_CODE.SELF_SIGNED_CERT_IN_CHAIN: |
67 | | - return new CertificateError(err.message, X509_ERR.UNTRUSTED_CHAIN); |
68 | | - case undefined: |
69 | | - break; |
70 | | - } |
71 | | - } |
72 | | - return err; |
73 | | - } |
74 | | - |
75 | | - // determineVerifyErrorCause fetches the certificate(s) from the specified |
76 | | - // address, parses the leaf, and returns the reason the certificate is giving |
77 | | - // an "unable to verify" error or throws if unable to figure it out. |
78 | | - private static async determineVerifyErrorCause( |
79 | | - address: string, |
80 | | - ): Promise<X509_ERR> { |
81 | | - return new Promise((resolve, reject) => { |
82 | | - try { |
83 | | - const url = new URL(address); |
84 | | - const socket = tls.connect( |
85 | | - { |
86 | | - port: Number.parseInt(url.port, 10) || 443, |
87 | | - host: url.hostname, |
88 | | - rejectUnauthorized: false, |
89 | | - }, |
90 | | - () => { |
91 | | - const x509 = socket.getPeerX509Certificate(); |
92 | | - socket.destroy(); |
93 | | - if (!x509) { |
94 | | - throw new Error("no peer certificate"); |
95 | | - } |
96 | | - |
97 | | - // We use "@peculiar/x509" because Node's x509 returns an undefined `keyUsage`. |
98 | | - const cert = new X509Certificate(x509.toString()); |
99 | | - const isSelfIssued = cert.subject === cert.issuer; |
100 | | - if (!isSelfIssued) { |
101 | | - return resolve(X509_ERR.PARTIAL_CHAIN); |
102 | | - } |
103 | | - |
104 | | - // The key usage needs to exist but not have cert signing to fail. |
105 | | - const extension = cert.getExtension(KeyUsagesExtension); |
106 | | - if (extension) { |
107 | | - const hasKeyCertSign = |
108 | | - extension.usages & KeyUsageFlags.keyCertSign; |
109 | | - if (!hasKeyCertSign) { |
110 | | - return resolve(X509_ERR.NON_SIGNING); |
111 | | - } |
112 | | - } |
113 | | - // This branch is currently untested; it does not appear possible to |
114 | | - // get the error "unable to verify" with a self-signed certificate |
115 | | - // unless the key usage was the issue since it would have errored |
116 | | - // with "self-signed certificate" instead. |
117 | | - return resolve(X509_ERR.UNTRUSTED_LEAF); |
118 | | - }, |
119 | | - ); |
120 | | - socket.on("error", (err) => reject(toError(err))); |
121 | | - } catch (err) { |
122 | | - reject(toError(err)); |
123 | | - } |
124 | | - }); |
125 | | - } |
126 | | - |
127 | | - // allowInsecure updates the value of the "coder.insecure" property. |
128 | | - private allowInsecure(): void { |
129 | | - vscode.workspace |
130 | | - .getConfiguration() |
131 | | - .update("coder.insecure", true, vscode.ConfigurationTarget.Global); |
132 | | - vscode.window.showInformationMessage(CertificateError.InsecureMessage); |
133 | | - } |
134 | | - |
135 | | - async showModal(title: string): Promise<void> { |
136 | | - return this.showNotification(title, { |
137 | | - detail: this.x509Err || this.message, |
138 | | - modal: true, |
139 | | - useCustom: true, |
140 | | - }); |
141 | | - } |
142 | | - |
143 | | - async showNotification( |
| 1 | +/** |
| 2 | + * Base class for certificate-related errors that can display notifications to users. |
| 3 | + * Use `instanceof CertificateError` to check if an error is a certificate error. |
| 4 | + */ |
| 5 | +export abstract class CertificateError extends Error { |
| 6 | + /** Human-friendly detail message for display */ |
| 7 | + public abstract readonly detail: string; |
| 8 | + |
| 9 | + /** Show error notification. Pass { modal: true } for modal dialogs. */ |
| 10 | + public abstract showNotification( |
144 | 11 | title?: string, |
145 | | - options: vscode.MessageOptions = {}, |
146 | | - ): Promise<void> { |
147 | | - const val = await vscode.window.showErrorMessage( |
148 | | - title || this.x509Err || this.message, |
149 | | - options, |
150 | | - // TODO: The insecure setting does not seem to work, even though it |
151 | | - // should, as proven by the tests. Even hardcoding rejectUnauthorized to |
152 | | - // false does not work; something seems to just be different when ran |
153 | | - // inside VS Code. Disabling the "Strict SSL" setting does not help |
154 | | - // either. For now avoid showing the button until this is sorted. |
155 | | - // CertificateError.ActionAllowInsecure, |
156 | | - CertificateError.ActionOK, |
157 | | - ); |
158 | | - switch (val) { |
159 | | - case CertificateError.ActionOK: |
160 | | - case undefined: |
161 | | - return; |
162 | | - } |
163 | | - } |
| 12 | + options?: { modal?: boolean }, |
| 13 | + ): Promise<void>; |
164 | 14 | } |
0 commit comments