为IP地址生成自签名证书

如题

问题描述

为指定的域名或者IP地址生成自签名证书, 要求使用HTTPS协议访问时, 主流浏览器Chrome, firefox等不弹出警告.

这里的自签名证书是指可根据IP或域名动态生成的二级证书.不借由第三方权威颁发机构生成.

举个简单的场景, A机在指定端口起了服务后, B机在浏览器中想要通过HTTPS协议访问A机服务器, 浏览器不警告.

通常的服务器证书, 我们是向let’s Encript或其他权威机构申请, 而这里, 要求A机服务器的证书是由自己生成的CA机构颁发.

解决方法

解决思路:

先生成一个根证书颁发机构 (Root certificate authority), 然后基于颁发机构生成二级证书, 在二级证书中绑定域名或者IP地址.

针对问题描述中的提到的例子, 相当于在A机生成根证书颁发机构RCA 和基于该颁发机构生成的绑定A机IP地址的二级证书,然后B机通过某种方式下载了该 RCA, 同时系统设置信任[相当危险], 这样便可走HTTPS协议访问A机的服务了.

下面分传统的openssl和nodejs两个方式来实现下.

  • Openssl方式

    先生成根证书:

    新增一个shell 脚本文件generate_root_ca.sh, 放入如下内容, 然后执行脚本即可.[具体各参数的含义, 可参考x509v3 config]

    #!/bin/sh
    
    echo "[req]
    default_bits  = 2048
    distinguished_name = req_distinguished_name
    x509_extensions = v3_req
    prompt = no
    [req_distinguished_name]
    countryName = XX
    stateOrProvinceName = N/A
    localityName = N/A
    organizationName = Self-signed Cert
    commonName = Self-signed Cert
    [v3_req]
    basicConstraints = CA:TRUE
    " > root.cnf
    
    openssl req -x509 -nodes -days 730 -newkey rsa:2048 -keyout rootCA.key -out rootCA.crt -config root.cnf
    
    rm root.cnf
    

    此处的关键在于basicConstraints 设置为 CA:TRUE

    也可以直接在终端生成, 执行:

    # 生成private key, 可以添加选项 -des3 来给 private key 设置密码.这里略过
    openssl genrsa  -out rootCA.key 2048
    
    # 生成cert, -days 用于设置过期时间, 指定加密算法为 sha256, 执行后会有提示, 设置即可
    openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 730 -out rootCA.crt
    

生成后得让计算机信任这个机构. 以MacOS为例:

打开keychain Access, file > import items 选中生成的rootCA.crt文件, 双击该文件, 选择 Always Trust.

这样,基于该机构颁发的证书, 都是被本机信任的.

现在我们来生成基于该CA颁发的证书:

以IP地址为例, 新增shell脚本文件 generate_ip_cert.sh, 添加如下内容:

IP=$(echo $1)
echo "
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
IP.1 = $IP
" > cert.cnf

openssl genrsa -out key.pem 2048
openssl req -new -key key.pem -out csr.pem -subj "/C=XX/ST=MyST"
openssl x509 -req -in csr.pem -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out cert.pem -days 730 -sha256 -extfile cert.cnf
rm cert.cnf

执行./generate_ip_cert.sh IPAddress [IPAddress为你需要为其生成证书的IP地址]

如果为指定的域名生成证书, 只需修改alt_names部分即可:

DNS=$(echo $1)

echo "
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = $DNS
" > cert.cnf

openssl genrsa -out key.pem 2048
openssl req -new -key key.pem -out csr.pem -subj "/C=XX/ST=MyST"
openssl x509 -req -in csr.pem -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out cert.pem -days 730 -sha256 -extfile cert.cnf
rm cert.cnf

同样也可以在终端一步步走openssl 生成, 这里略过.

随后将生成的cert.pem, key.pem放进服务器的配置中即可.

  • Nodejs 方式

    原理同上面的一样, 这里用一个小的express实例来实验下.

    为了生成证书, 首先我们需要引用了一个开源的package: selfsigned, 因为原来的库并不支持基于Root CA生成证书, 这里我们用Andrey Novikov fork后的改进版本.

    直接起一个express, entry point为app.js. 在package.json中添加:

    "selfsigned": "https://github.com/Envek/selfsigned.git#7477718"
    

    随后npm install.

    app.js中添加如下内容:

    const express = require('express')
    const https = require('https')
    const fs = require('fs')
    const ip = require('ip')
    const selfsigned = require('selfsigned')
    
    const app = express()
    const address = ip.address()
    const port = 3008
    
    fs.mkdirSync('./ca', { recursive: true })
    const rootCA = selfsigned.generate(
      [
        { name: 'commonName', value: 'Self-signedCert' },
        { name: 'countryName', value: 'XX' },
        { name: 'organizationName', value: 'Myorg' },
      ],
      {
        keySize: 2048,
        algorithm: 'sha256',
        extensions: [
          {
            name: 'basicConstraints',
            cA: true,
          },
        ]
      }
    )
    
    fs.writeFileSync('./ca/rootCA.crt', rootCA.cert)
    
    const pems = selfsigned.generate(
      [
        { name: 'commonName', value: 'Self-signedCert' },
        { name: 'countryName', value: 'XX' },
        { name: 'organizationName', value: 'Myorg' },
      ],
      {
        keySize: 2048,
        ca: rootCA,
        algorithm: 'sha256',
        extensions: [
          {
            name: 'basicConstraints',
            cA: false,
          },
          {
            name: "keyUsage",
            keyCertSign: false,
            digitalSignature: true,
            nonRepudiation: true,
            keyEncipherment: true,
            dataEncipherment: true,
          },
          {
            name: "extKeyUsage",
            serverAuth: true,
            clientAuth: true,
            codeSigning: true,
            timeStamping: true,
          },
          {
            name: "subjectAltName",
            altNames: [
              {
                type: 7,
                ip: address,
              },
            ],
          },
        ],
      }
    )
    
    fs.writeFileSync('./ca/cert.pem', pems.cert)
    fs.writeFileSync('./ca/key.pem', pems.private)
    
    app.get('/', (req, res) => {
      res.send('Hello World!')
    })
    
    https.createServer({
      key: fs.readFileSync('./ca/key.pem'),
      cert: fs.readFileSync('./ca/cert.pem')
    }, app).listen(port, () => {
      console.log(`listening on https://${address}:${port}`)
    })
    

    将项目的ca目录下的rootCA.crt 拖到 keychain中, 并设置为信任的机构.

    终端执行node app.js:

    浏览器访问正常.可以点开🔒️的图标, 查看我们生成的二级证书:

最后, 值得一提的是Firefox, 系统设置信任对Firefox并不起效, 需要在perferences 中将生成的rootCA.crt 导入进去才可以. [此为危险操作]

参考