UNPKG

ssl-crt

Version:

## 使用 ### 命令行 ``` npm install create-self-signed -g self-signed --help ```

554 lines (524 loc) 16.4 kB
const { exec, execSync } = require('child_process') const fse = require('fs-extra') const fs = require('fs') const inquirer = require('inquirer') // 判断系统,暂时这样写,不确定准确性 const isOSX = process.platform === 'darwin' const sslCertificateDir = `${process.env.HOME}/.self-signed-cert` const sslConfigFile = `${sslCertificateDir}/ssl.cnf` const sslKeyPath = `${sslCertificateDir}/ssl.key` const sslCrtPath = `${sslCertificateDir}/ssl.crt` // 安装到系统上证书的名称 const CN = 'genereted by create-self-signed' // const CN = 'genereted by ssl-cert-tool@yingchun.fyc' // const CN = 'self-signed-cert genereted by ssl-cert-tool@yingchun.fyc' // 自签名证书默认支持的域名 const DEFAULTDOMAINS = [ 'localhost', '*.taobao.com', '*.alibaba-inc.com', '*.alimama.com', '*.tanx.com', '*.m.taobao.com' ] const questions = [ /* { type: 'input', name: 'C', message: 'Country Name (2 letter code) [CN]', default: 'CN' }, { type: 'input', name: 'ST', message: 'State or Province Name (full name) [ZheJiang]', default: 'ZheJiang' }, { type: 'input', name: 'L', message: 'Locality Name (eg, city) []', default: 'HangZhou' }, { type: 'input', name: 'O', message: 'Organization Name (eg, company) [Internet Widgits Pty Ltd]', default: 'Alibaba' }, { type: 'input', name: 'OU', message: 'Organizational Unit Name (eg, section) []', default: 'AlimamaMUX' }, */{ type: 'input', name: 'domains', // message: 'Input the domain name to be self-signed. Separate multiple domain names with commas', message: '输入启动本地HTTPS服务时使用的域名,多个以,分隔,直接回车将使用默认', default: DEFAULTDOMAINS.join(',') }] const getInquirerAnswer = async () => { const answer = await inquirer.prompt(questions) let { domains } = answer if (domains) { domains = domains.split(',').map(item => item.trim()) } else { domains = [] } let allDomains = DEFAULTDOMAINS.concat(domains) // 过滤重复的 allDomains = allDomains.reduce((accumulator, currentValue) => { !accumulator.includes(currentValue) && accumulator.push(currentValue) return accumulator }, []) return { ...answer, hosts: allDomains } } const createCnfFile = ({ hosts }) => { fs.writeFileSync(sslConfigFile, ` [req] prompt = no default_bits = 4096 default_md = sha256 distinguished_name = dn x509_extensions = v3_req [dn] C=CN ST=ZheJiang L=HangZhou O=Alibaba OU=AlimamaMux CN=${CN} emailAddress=yingchun.fyc@alibaba-inc.com [v3_req] keyUsage=keyEncipherment, dataEncipherment extendedKeyUsage=serverAuth subjectAltName=@alt_names [alt_names] ${hosts.map((item, index) => { return `DNS.${index + 1} = ${item}` }).join('\n')} IP.1 = 127.0.0.1 `.trim()) } /** * 通过命令行的交互方式收集配置信息,然后生成配置文件 */ const createConfigFile = async (options) => { await fse.ensureDir(sslCertificateDir) createCnfFile(options) } /** * 创建密钥和自签名证书 */ const createSSLKeyAndCrt = () => new Promise((resolve, reject) => { // 通过 .cnf 生成 .key、.crt exec(`openssl req \ -new \ -newkey rsa:2048 \ -sha1 \ -days 3650 \ -nodes \ -x509 \ -keyout ${sslKeyPath} \ -out ${sslCrtPath} \ -config ${sslConfigFile}`, (error, stdout, stderr) => { if (error) { // console.log('*error*') // console.log(stderr) resolve({ success: false }) return } resolve({ success: true, sslKeyPath, sslCrtPath }) }) }) /** * OSX 在系统钥匙串里添加并始终信任自签名证书 */ const trustSelfSignedCert = () => new Promise((resolve, reject) => { exec(`sudo security add-trusted-cert \ -d -r trustRoot \ -k /Library/Keychains/System.keychain \ ${sslCrtPath}`, (error, stdout, stderr) => { if (error) { resolve(false) return } resolve(true) }) }) /** * 获取该工具添加到钥匙串里自签名证书列表,返回的是证书的sha-1列表 */ const getKeyChainCertSha1List = () => { let sha1List if (isOSX) { try { const sha1Str = isOSX && execSync(`security find-certificate -a -c '${CN}' -Z | grep ^SHA-1`, { encoding: 'utf-8' }) sha1List = sha1Str.replace(/SHA-1\shash:\s/g, '').split('\n').filter(sha1 => sha1) } catch (error) { // 找不到 sha1List = [] } } else { sha1List = [] } return sha1List } /** * 卸载证书与删除信任 */ const unInstall = async () => { const sha1List = getKeyChainCertSha1List() const existCrtDir = fs.existsSync(sslCertificateDir) if (!sha1List.length && !existCrtDir) { console.log('没有找到该工具安装的证书') // console.log('没有找到该工具创建的自签名证书,钥匙串里也没找到该工具添加的证书') return true } // const sha1 = execSync(`child.execSync('openssl x509 -sha1 -in ${sslCrtPath} -noout -fingerprint',{encoding: 'utf-8'}).split('=')[1].replace(/:/g, '').trim()`, {encoding: 'utf-8'}) if (sha1List.length) { console.log(`正在删除钥匙串里名称「${CN}」的证书`) try { sha1List.forEach(sha1 => { execSync(`sudo security delete-certificate -Z ${sha1}`) }) } catch (error) { console.log('删除失败,流程结束') return false } console.log('删除成功') } if (existCrtDir) { execSync(`rm -rf ${sslCertificateDir}`) console.log(`已经删除存放密钥和证书的目录${sslCertificateDir}`) } console.log('删除完成') return true } /** * 创建自签名证书并信任 */ const install = async () => { const sha1List = getKeyChainCertSha1List() const existCrtDir = fs.existsSync(sslCertificateDir) if (sha1List.length || existCrtDir) { let message if (sha1List.length && existCrtDir) { message = `继续操作会删除钥匙串里名称是${CN}的证书和存放密钥和自签名证书的目录${sslCertificateDir}` } else if (sha1List.length) { message = `继续操作会删除钥匙串里名称是${CN}的证书` } else if (existCrtDir) { message = `继续操作会删除存放密钥和自签名证书文件的目录${sslCertificateDir}` } message = '继续操作会覆盖该工具已创建的证书' const answer = await inquirer.prompt([{ type: 'confirm', name: 'continue', message, default: false }]) // 取消卸载或卸载失败 if (!(answer.continue && await unInstall())) return if (!answer.continue) return } const options = await getInquirerAnswer() await createConfigFile(options) const result = await createSSLKeyAndCrt() if (result.success) { // console.log('generate ssl certificate successfully ^.^') console.log('成功创建密钥和自签名证书') // console.log('private key: ', sslKeyPath) // console.log('certificate: ', sslCrtPath) } // console.log('try add trust to system.keychain, need you permission') if (isOSX) { console.log('向系统的钥匙串里添加证书并始终信任...') const isTrustCert = await trustSelfSignedCert() if (isTrustCert) { console.log('添加并信任成功,钥匙串里名称为:', CN) } else { console.log('钥匙串添加证书失败') } } console.log('安装结束') console.log('') console.log('可随时通过下面命令行查看自签名信息') console.log('$ self-signed') console.log('') console.log('安装证书的结果:') currentState() } /** * 获取自签名证书里支持的域名 */ const getCrtHosts = () => { let hosts try { hosts = execSync(`openssl x509 -in ${sslCrtPath} -noout -text | grep DNS`, { encoding: 'utf-8' }).trim().split(',').filter(item => item.includes('DNS:')).map(item => item.trim().replace(/DNS:/, '')) } catch (error) { // return false return [] } return hosts } /** * 获取自己创建的证书密钥和自签名证书 * @param {Array} param0 */ const obtainSelfSigned = async (hosts = DEFAULTDOMAINS) => { const existSslKeyAndCrt = fs.existsSync(sslKeyPath) && fs.existsSync(sslCrtPath) let matched if (existSslKeyAndCrt) { const crtHosts = getCrtHosts() matched = hosts.every(host => { // m.tanx.com 能匹配上 *.tanx.com // (new RegExp('*.tanx.com'.replace('*','^[^.]+'))).test('m.tanx.com') // true return crtHosts.find(crtHostItem => { if (crtHostItem.includes('*')) { return (new RegExp(crtHostItem.replace('*', '^[^.]+'))).test(host) } else { return crtHostItem === host } }) }) if (!matched) { const addedHosts = hosts.filter(host => { return !crtHosts.find(crtHostItem => { if (crtHostItem.includes('*')) { return (new RegExp(crtHostItem.replace('*', '^[^.]+'))).test(host) } else { return crtHostItem === host } }) }) hosts = crtHosts.concat(addedHosts) } } if (existSslKeyAndCrt && matched) { const sha1List = getKeyChainCertSha1List() const sha1 = execSync(`openssl x509 -sha1 -in ${sslCrtPath} -noout -fingerprint`, { encoding: 'utf-8' }).split('=')[1].replace(/:/g, '').trim() let certTrusted if (sha1List.includes(sha1)) { certTrusted = true } else if (isOSX) { certTrusted = await trustSelfSignedCert() } return { success: true, sslKeyPath, sslCrtPath, certTrusted } } else if (existSslKeyAndCrt) { // console.log('已有的自签名证书里没有你需要的域名') // const answer = await inquirer.prompt([{ // type: 'confirm', // name: 'continue', // message: '是否更新自签名证书,新增要支持的域名', // default: false // }]) if (isOSX) { const sha1List = getKeyChainCertSha1List() // todo 要加sha-1比对才准确 if (sha1List.length) { // console.log('自签名证书要新增支持的域名,正在更新自签名证书,需要重新信任') console.log('新增域名需要更新证书并重新信任') } else { console.log('新增域名需要更新证书') // console.log('自签名证书要新增支持的域名,正在更新自签名证书') } try { sha1List.forEach(sha1 => { execSync(`sudo security delete-certificate -Z ${sha1}`) }) } catch (error) { return { success: false, message: '卸载老的证书失败,请授权重试' // sslKeyPath, // sslCrtPath, } } } else { console.log('自签名证书要新增支持的域名,正在更新自签名证书') } execSync(`rm -rf ${sslCertificateDir}`) } await createConfigFile({ hosts }) const result = await createSSLKeyAndCrt() if (result.success) { let certTrusted = false if (isOSX) { certTrusted = await trustSelfSignedCert() } return { success: true, sslKeyPath, sslCrtPath, certTrusted } } } /** * 自签名信息 */ const currentState = () => { const existSslKeyAndCrt = fs.existsSync(sslKeyPath) && fs.existsSync(sslCrtPath) if (!existSslKeyAndCrt) { console.log('还没有安装自签名证书,运行下面命令安装使用') console.log('$ self-signed install') return } console.log('密钥文件路径:', sslKeyPath) console.log('自签名证书文件路径:', sslCrtPath) console.log('自签名证书已经支持的域名:') const crtHosts = getCrtHosts() console.log(crtHosts.join(',')) console.log('自签名证书的有效时间:') console.log(`${execSync(`openssl x509 -in ${sslCrtPath} -noout -dates`, { encoding: 'utf-8' })}`.trim()) if (isOSX) { const sha1List = getKeyChainCertSha1List() const sha1 = execSync(`openssl x509 -sha1 -in ${sslCrtPath} -noout -fingerprint`, { encoding: 'utf-8' }).split('=')[1].replace(/:/g, '').trim() if (sha1List.includes(sha1)) { console.log('自签名证书已经添加到钥匙串并被始终信任') console.log(`自签名证书在钥匙串里的名称:${CN}`) console.log(`自签名证书在钥匙串里的sha-1:${sha1}`) } else { console.log('自签名证书还没被添加到钥匙串,可以运行下面命令,执行添加和始终信任') console.log('$ self-signed trust') } } console.log('') console.log('更多使用帮助') console.log('$ self-signed --help') console.log('') console.log('如何配置服务器的HTTPS') console.log('https://yuque.antfin-inc.com/yingchun.fyc/kwg02h/ludtut#60d30e4c') console.log('') console.log('如有疑问联系author@慧知') } /** * 信任自签名证书 */ const trustSelfSigned = async () => { const existSslKeyAndCrt = fs.existsSync(sslKeyPath) && fs.existsSync(sslCrtPath) if (!existSslKeyAndCrt) { console.log('还没有安装自签名证书,运行下面命令安装使用') console.log('$ self-signed install') return } const sha1List = getKeyChainCertSha1List() const sha1 = execSync(`openssl x509 -sha1 -in ${sslCrtPath} -noout -fingerprint`, { encoding: 'utf-8' }).split('=')[1].replace(/:/g, '').trim() if (sha1List.includes(sha1)) { console.log('钥匙串里已经添加过,无须重复添加') console.log('在钥匙串里证书的信息:') console.log(`名称: ${CN}`) console.log(`sha-1: ${sha1}`) } else { const added = await trustSelfSignedCert() if (added) { // console.log(`已成功添加自签名,名称是${CN},sha-1是${sha1}`) console.log(`添加并信任成功,钥匙串里名称是"${CN}"`) } else { console.log('添加失败') } } } /** * 添加新的要支持的域名 * @param {*} hosts */ const addHosts = async (hosts = []) => { if (!hosts.length) { console.log('输入要支持的host') return } const existSslKeyAndCrt = fs.existsSync(sslKeyPath) && fs.existsSync(sslCrtPath) if (!existSslKeyAndCrt) { console.log('还没有安装自签名证书,运行下面命令安装使用') console.log('$ self-signed install') return } const crtHosts = getCrtHosts() const matched = hosts.every(host => { return crtHosts.find(crtHostItem => { if (crtHostItem.includes('*')) { return (new RegExp(crtHostItem.replace('*', '^[^.]+'))).test(host) } else { return crtHostItem === host } }) }) if (matched) { console.log('证书已经支持该域名,无须添加了') return } if (!matched) { console.log(crtHosts) const addedHosts = hosts.filter(host => { return !crtHosts.find(crtHostItem => { if (crtHostItem.includes('*')) { return (new RegExp(crtHostItem.replace('*', '^[^.]+'))).test(host) } else { return crtHostItem === host } }) }) console.log(addedHosts) hosts = crtHosts.concat(addedHosts) } let sha1List if (isOSX) { sha1List = getKeyChainCertSha1List() // todo 要加sha-1比对才准确 if (sha1List.length) { console.log('新增域名需要更新证书并重新信任') } else { console.log('正在更新证书') } try { sha1List.forEach(sha1 => { execSync(`sudo security delete-certificate -Z ${sha1}`) }) } catch (error) { console.log('新增域名失败') return } } else { console.log('正在更新证书') } execSync(`rm -rf ${sslCertificateDir}`) await createConfigFile({ hosts }) const result = await createSSLKeyAndCrt() if (result.success) { let certTrusted = false if (isOSX && sha1List.length) { certTrusted = trustSelfSignedCert() } console.log('更新成功') console.log('更新后支持的域名') const crtHosts = getCrtHosts() console.log(crtHosts.join(',')) console.log('更新后证书的起止有效时间:') console.log(`${execSync(`openssl x509 -in ${sslCrtPath} -noout -dates`, { encoding: 'utf-8' })}`.trim()) } } const removeSelfSigned = () => { } module.exports = { obtainSelfSigned, removeSelfSigned, currentState, install, unInstall, trustSelfSigned, addHosts }