111 lines
3.9 KiB
JavaScript
111 lines
3.9 KiB
JavaScript
import { execFile } from 'node:child_process'
|
|
import { access, chmod, mkdir, rm, writeFile } from 'node:fs/promises'
|
|
import { constants } from 'node:fs'
|
|
import { homedir, platform } from 'node:os'
|
|
import { dirname, join } from 'node:path'
|
|
import { promisify } from 'node:util'
|
|
import { configPath } from './connector-config.mjs'
|
|
|
|
const run = promisify(execFile)
|
|
|
|
export function certificateDirectory() {
|
|
return process.env.SPINE_CONNECTOR_TLS_DIR || join(dirname(configPath()), 'tls')
|
|
}
|
|
|
|
export function certificatePaths(directory = certificateDirectory()) {
|
|
return {
|
|
directory,
|
|
caKey: join(directory, 'ca-key.pem'),
|
|
ca: join(directory, 'ca.pem'),
|
|
serverKey: join(directory, 'server-key.pem'),
|
|
serverCertificate: join(directory, 'server.pem'),
|
|
identity: join(directory, 'identity.p12'),
|
|
request: join(directory, 'server.csr'),
|
|
extensions: join(directory, 'server.ext'),
|
|
}
|
|
}
|
|
|
|
async function certificateIsUsable(path) {
|
|
try {
|
|
await access(path, constants.R_OK)
|
|
await run('/usr/bin/openssl', ['x509', '-checkend', '2592000', '-noout', '-in', path])
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
async function identityIsUsable(path) {
|
|
try {
|
|
await access(path, constants.R_OK)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
async function runOpenSSL(args) {
|
|
try {
|
|
await run('/usr/bin/openssl', args)
|
|
} catch (error) {
|
|
const detail = String(error?.stderr || error?.message || error).trim()
|
|
throw new Error(`无法生成本机 HTTPS 证书:${detail}`)
|
|
}
|
|
}
|
|
|
|
export async function ensureLocalCertificate() {
|
|
if (platform() !== 'darwin') throw new Error('Safari 本机 HTTPS 证书仅支持 macOS')
|
|
const paths = certificatePaths()
|
|
if (await certificateIsUsable(paths.serverCertificate) && await identityIsUsable(paths.identity)) return paths
|
|
|
|
await mkdir(paths.directory, { recursive: true, mode: 0o700 })
|
|
await runOpenSSL(['genrsa', '-out', paths.caKey, '2048'])
|
|
await runOpenSSL([
|
|
'req', '-x509', '-new', '-key', paths.caKey, '-sha256', '-days', '3650',
|
|
'-subj', '/CN=Spine Particle Connector Local CA', '-out', paths.ca,
|
|
])
|
|
await runOpenSSL(['genrsa', '-out', paths.serverKey, '2048'])
|
|
await runOpenSSL(['req', '-new', '-key', paths.serverKey, '-subj', '/CN=127.0.0.1', '-out', paths.request])
|
|
await writeFile(paths.extensions, 'subjectAltName=IP:127.0.0.1,DNS:localhost\nextendedKeyUsage=serverAuth\nbasicConstraints=CA:FALSE\n', 'utf8')
|
|
await runOpenSSL([
|
|
'x509', '-req', '-in', paths.request, '-CA', paths.ca, '-CAkey', paths.caKey,
|
|
'-CAcreateserial', '-out', paths.serverCertificate, '-days', '825', '-sha256',
|
|
'-extfile', paths.extensions,
|
|
])
|
|
await runOpenSSL([
|
|
'pkcs12', '-export', '-out', paths.identity, '-inkey', paths.serverKey,
|
|
'-in', paths.serverCertificate, '-passout', 'pass:spine-connector-local',
|
|
])
|
|
await Promise.all([paths.serverKey, paths.identity].map((path) => chmod(path, 0o600)))
|
|
await rm(paths.caKey, { force: true })
|
|
return paths
|
|
}
|
|
|
|
export async function localCertificateTrusted() {
|
|
const paths = certificatePaths()
|
|
try {
|
|
await run('/usr/bin/security', [
|
|
'verify-cert', '-c', paths.serverCertificate, '-n', '127.0.0.1', '-p', 'ssl', '-L', '-q',
|
|
])
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
export async function trustLocalCertificate() {
|
|
if (platform() !== 'darwin') throw new Error('Safari 本机 HTTPS 证书仅支持 macOS')
|
|
const paths = await ensureLocalCertificate()
|
|
const loginKeychain = join(homedir(), 'Library', 'Keychains', 'login.keychain-db')
|
|
try {
|
|
await run('/usr/bin/security', [
|
|
'add-trusted-cert', '-r', 'trustRoot', '-p', 'ssl', '-k', loginKeychain, paths.ca,
|
|
])
|
|
} catch (error) {
|
|
const detail = String(error?.stderr || error?.message || error).trim()
|
|
throw new Error(detail || '无法把本机证书加入信任列表')
|
|
}
|
|
if (!(await localCertificateTrusted())) throw new Error('证书已安装,但系统仍未信任它')
|
|
return paths
|
|
}
|