Skip to content

Fix self signed certificate for iPad #2255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Nov 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ci/dev/fmt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ main() {
doctoc --title '# Install' doc/install.md > /dev/null
doctoc --title '# npm Install Requirements' doc/npm.md > /dev/null
doctoc --title '# Contributing' doc/CONTRIBUTING.md > /dev/null
doctoc --title '# iPad' doc/ipad.md > /dev/null

if [[ ${CI-} && $(git ls-files --other --modified --exclude-standard) ]]; then
echo "Files need generation or are formatted incorrectly:"
Expand Down
18 changes: 8 additions & 10 deletions doc/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# FAQ

- [Questions?](#questions)
- [iPad Status?](#ipad-status)
- [How can I reuse my VS Code configuration?](#how-can-i-reuse-my-vs-code-configuration)
- [Differences compared to VS Code?](#differences-compared-to-vs-code)
- [How can I request a missing extension?](#how-can-i-request-a-missing-extension)
Expand All @@ -21,7 +22,6 @@
- [Heartbeat File](#heartbeat-file)
- [Healthz endpoint](#healthz-endpoint)
- [How does the config file work?](#how-does-the-config-file-work)
- [Blank screen on iPad?](#blank-screen-on-ipad)
- [Isn't an install script piped into sh insecure?](#isnt-an-install-script-piped-into-sh-insecure)
- [How do I make my keyboard shortcuts work?](#how-do-i-make-my-keyboard-shortcuts-work)
- [Differences compared to Theia?](#differences-compared-to-theia)
Expand All @@ -33,6 +33,10 @@

Please file all questions and support requests at https://github.com/cdr/code-server/discussions.

## iPad Status?

Please see [./ipad.md](./ipad.md).

## How can I reuse my VS Code configuration?

The very popular [Settings Sync](https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync) extension works.
Expand Down Expand Up @@ -144,6 +148,9 @@ For HTTPS, you can use a self signed certificate by passing in just `--cert` or
pass in an existing certificate by providing the path to `--cert` and the path to
the key with `--cert-key`.

The self signed certificate will be generated into
`~/.local/share/code-server/self-signed.crt`.

If `code-server` has been passed a certificate it will also respond to HTTPS
requests and will redirect all HTTP requests to HTTPS.

Expand Down Expand Up @@ -279,15 +286,6 @@ The `--config` flag or `$CODE_SERVER_CONFIG` can be used to change the config fi

The default location also respects `$XDG_CONFIG_HOME`.

## Blank screen on iPad?

Unfortunately at the moment self signed certificates cause a blank screen on iPadOS

There does seem to be a way to get it to work if you create your own CA and create a
certificate using the CA and then import the CA onto your iPad.

See [#1566](https://github.com/cdr/code-server/issues/1566#issuecomment-623159434).

## Isn't an install script piped into sh insecure?

Please give
Expand Down
3 changes: 1 addition & 2 deletions doc/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,7 @@ Visit `https://<your-domain-name>` to access `code-server`. Congratulations!

### Self Signed Certificate

**note:** Self signed certificates do not work with iPad and will cause a blank page. You'll
have to use [Let's Encrypt](#lets-encrypt) instead. See the [FAQ](./FAQ.md#blank-screen-on-ipad).
**note:** Self signed certificates do not work with iPad normally. See [./ipad.md](./ipad.md) for details.

Recommended reading: https://security.stackexchange.com/a/8112.

Expand Down
56 changes: 56 additions & 0 deletions doc/ipad.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
# iPad

- [iPad](#ipad)
- [Known Issues](#known-issues)
- [How to access code-server with a self signed certificate on iPad?](#how-to-access-code-server-with-a-self-signed-certificate-on-ipad)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

# iPad

## Known Issues

- Getting self signed certificates certificates to work is involved, see below.
- Keyboard may disappear sometimes [#1313](https://github.com/cdr/code-server/issues/1313), [#979](https://github.com/cdr/code-server/issues/979)
- Trackpad scrolling does not work [#1455](https://github.com/cdr/code-server/issues/1455)
- See [issues tagged with the iPad label](https://github.com/cdr/code-server/issues?q=is%3Aopen+is%3Aissue+label%3AiPad) for more.

## How to access code-server with a self signed certificate on iPad?

Accessing a self signed certificate on iPad isn't as easy as accepting through all
the security warnings. Safari will prevent WebSocket connections unless the certificate
is installed as a profile on the device.

The below assumes you are using the self signed certificate that code-server
generates for you. If not, that's fine but you'll have to make sure your certificate
abides by the following guidelines from Apple: https://support.apple.com/en-us/HT210176

**note**: Another undocumented requirement we noticed is that the certificate has to have `basicConstraints=CA:true`.

The following instructions assume you have code-server installed and running
with a self signed certificate. If not, please first go through [./guide.md](./guide.md)!

**warning**: Your iPad must access code-server via a domain name. It could be local
DNS like `mymacbookpro.local` but it must be a domain name. Otherwise Safari will
refuse to allow WebSockets to connect.

1. Your certificate **must** have a subject alt name that matches the hostname
at which you will access code-server from your iPad. You can pass this to code-server
so that it generates the certificate correctly with `--cert-host`.
2. Share your self signed certificate with the iPad.
- code-server will print the location of the certificate it has generated in the logs.

```
[2020-10-30T08:55:45.139Z] info - Using generated certificate and key for HTTPS: ~/.local/share/code-server/mymbp_local.crt
```

- You can mail it to yourself or if you have a Mac, it's easiest to just Airdrop to the iPad.

3. When opening the `*.crt` file, you'll be prompted to go into settings to install.
4. Go to `Settings -> General -> Profile`, select the profile and then hit `Install`.
- It should say the profile is verified.
5. Go to `Settings -> About -> Certificate Trust Settings` and enable full trust for
the certificate.
6. Now you can access code-server! 🍻
7 changes: 6 additions & 1 deletion src/node/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface Args extends VsArgs {
readonly auth?: AuthType
readonly password?: string
readonly cert?: OptionalString
readonly "cert-host"?: string
readonly "cert-key"?: string
readonly "disable-telemetry"?: boolean
readonly help?: boolean
Expand Down Expand Up @@ -101,7 +102,11 @@ const options: Options<Required<Args>> = {
cert: {
type: OptionalString,
path: true,
description: "Path to certificate. Generated if no path is provided.",
description: "Path to certificate. A self signed certificate is generated if none is provided.",
},
"cert-host": {
type: "string",
description: "Hostname to use when generating a self signed certificate.",
},
"cert-key": { type: "string", path: true, description: "Path to certificate key when using non-generated cert." },
"disable-telemetry": { type: "boolean", description: "Disable telemetry." },
Expand Down
4 changes: 2 additions & 2 deletions src/node/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ const main = async (args: Args, configArgs: Args): Promise<void> => {
proxyDomains: args["proxy-domain"],
socket: args.socket,
...(args.cert && !args.cert.value
? await generateCertificate()
? await generateCertificate(args["cert-host"] || "localhost")
: {
cert: args.cert && args.cert.value,
certKey: args["cert-key"],
Expand Down Expand Up @@ -209,7 +209,7 @@ const main = async (args: Args, configArgs: Args): Promise<void> => {
logger.info(
args.cert && args.cert.value
? ` - Using provided certificate and key for HTTPS`
: ` - Using generated certificate and key for HTTPS`,
: ` - Using generated certificate and key for HTTPS: ${humanPath(options.cert)}`,
)
} else {
logger.info(" - Not serving HTTPS")
Expand Down
44 changes: 32 additions & 12 deletions src/node/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,25 +54,45 @@ export function humanPath(p?: string): string {
return p.replace(os.homedir(), "~")
}

export const generateCertificate = async (): Promise<{ cert: string; certKey: string }> => {
const paths = {
cert: path.join(tmpdir, "self-signed.cert"),
certKey: path.join(tmpdir, "self-signed.key"),
}
const checks = await Promise.all([fs.pathExists(paths.cert), fs.pathExists(paths.certKey)])
export const generateCertificate = async (hostname: string): Promise<{ cert: string; certKey: string }> => {
const certPath = path.join(paths.data, `${hostname.replace(/\./g, "_")}.crt`)
const certKeyPath = path.join(paths.data, `${hostname.replace(/\./g, "_")}.key`)

const checks = await Promise.all([fs.pathExists(certPath), fs.pathExists(certKeyPath)])
if (!checks[0] || !checks[1]) {
// Require on demand so openssl isn't required if you aren't going to
// generate certificates.
const pem = require("pem") as typeof import("pem")
const certs = await new Promise<import("pem").CertificateCreationResult>((resolve, reject): void => {
pem.createCertificate({ selfSigned: true }, (error, result) => {
return error ? reject(error) : resolve(result)
})
pem.createCertificate(
{
selfSigned: true,
commonName: hostname,
config: `
[req]
req_extensions = v3_req

[ v3_req ]
basicConstraints = CA:true
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = ${hostname}
`,
},
(error, result) => {
return error ? reject(error) : resolve(result)
},
)
})
await fs.mkdirp(tmpdir)
await Promise.all([fs.writeFile(paths.cert, certs.certificate), fs.writeFile(paths.certKey, certs.serviceKey)])
await fs.mkdirp(paths.data)
await Promise.all([fs.writeFile(certPath, certs.certificate), fs.writeFile(certKeyPath, certs.serviceKey)])
}
return {
cert: certPath,
certKey: certKeyPath,
}
return paths
}

export const generatePassword = async (length = 24): Promise<string> => {
Expand Down
2 changes: 1 addition & 1 deletion test/socket.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe("SocketProxyProvider", () => {
}

before(async () => {
const cert = await generateCertificate()
const cert = await generateCertificate("localhost")
const options = {
cert: fs.readFileSync(cert.cert),
key: fs.readFileSync(cert.certKey),
Expand Down