大家在自己假设服务的时候肯定都遇到过这种情况,就是 HTTPS 的 web 界面浏览器不信任,只能手动添加 exception,添加完以后那个小锁还有个叹号lock mixed light 。如何把这个叹号小锁变成小绿锁 Secure 呢?这就涉及到自己架设 PKI 颁发证书的问题了。

基本概念

一个 PKI 的结构基本是这样的:系统里有一个根证书(root certificate),存在某个中心服务器(root CA)里。还有一些专门用来给别的服务器签证书的服务器,里面有数个中间证书(intermediate certificate),这些证书在这个 PKI 里是可信的,因为它们都是 root CA 签发的。最后每个 production 服务器上有从这些中间服务器使用中间证书签发的证书。在用户那里,根证书和中间证书都是受到信任的,所以这些 production 服务器的证书也是可信的。这些 production 证书会推导出其它一些密钥,用于通信加密。

具体操作

  1. 生成 root CA

  2. 生成 intermediate CA

  3. 用 root CA 签名 intermediate CA

  4. 用 intermediate CA 签发服务器证书

使用 CFSSL,这东西也没个正经的文档。我把需要的步骤包装成了 Python 脚本:

#!/usr/bin/env python

import sys, os
import json
import tempfile
import subprocess as subp

def getTempFile():
    with tempfile.NamedTemporaryFile(delete=False) as f:
        Name = f.name
    return Name

class ClosedTempFile(object):
    def __init__(self, delete=True):
        self.Filename = None
        self.Delete = delete

    def __enter__(self):
        self.Filename = getTempFile()
        return self.Filename

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.Filename is not None and self.Delete is True:
            os.remove(self.Filename)

def pipe2(cmd1, cmd2):
    Proc1 = subp.Popen(cmd1, stdout=subp.PIPE)
    subp.check_call(cmd2, stdin=Proc1.stdout)
    Proc1.wait()

def cmdBuildCA(args):
    # Create root CA.
    PayloadInit = {
        "CN": "Xeno Root CA",
        "key": {
            "algo": "ecdsa",
            "size": 384,
        },
        "names": [
            {
                "C": "U.S.",
                "O": "Xeno",
            }
        ],
        "ca": {
            "expiry": "262800h", # 30 years...
        }
    }

    with ClosedTempFile() as PayloadFile:
        with open(PayloadFile, 'w') as f:
            json.dump(PayloadInit, f)

        pipe2(("cfssl", "gencert", "-initca", PayloadFile),
              ("cfssljson", "-bare", args.Root))

    # Create intermediate CA.
    PayloadInter = {
        "CN": "Xeno Intermediate CA",
        "key": {
            "algo": "ecdsa",
            "size": 256
        },
        "names": [
            {
                "C": "U.S.",
                "O": "Xeno",
            }
        ],
        "ca": {
            "expiry": "42720h"
        }
    }

    with ClosedTempFile() as PayloadFile:
        with open(PayloadFile, 'w') as f:
            json.dump(PayloadInter, f)

        pipe2(("cfssl", "gencert", "-initca", PayloadFile),
              ("cfssljson", "-bare", args.Inter))

    # Sign intermediate CA cert.
    PayloadSign = {
        "signing": {
            "default": {
                "usages": ["digital signature", "cert sign",
                           "crl sign", "signing"],
                "expiry": "262800h",
                "ca_constraint": {"is_ca": True, "max_path_len": 0,
                                  "max_path_len_zero": True}
            }
        }
    }

    with ClosedTempFile() as PayloadFile:
        with open(PayloadFile, 'w') as f:
            json.dump(PayloadSign, f)

        pipe2(("cfssl", "sign", "-ca", args.Root + ".pem",
               "-ca-key", args.Root + "-key.pem", "-config", PayloadFile,
               args.Inter + ".csr"),
              ("cfssljson", "-bare", args.Inter))

    # TODO: generate CA bundle.
    with open(args.Output, 'w') as FileOutput:
        with open(args.Root + ".pem", 'r') as FileRoot:
            FileOutput.write(FileRoot.read())
        with open(args.Inter + ".pem", 'r') as FileInter:
            FileOutput.write(FileInter.read())

def cmdNewCert(args):
    # CSR
    PayloadCSR = {
        "CN": args.CommonName,
        "key": {
            "algo": "ecdsa",
            "size": 256
        },
        "names": [
            {
                "C": "U.S.",
                "O": "Xeno"
            }
        ],
        "hosts": args.Hosts,
    }

    PayloadConfig = {
        "signing": {
            "default": {
                "expiry": "43800h",
                "usages": ["signing", "key encipherment", "client auth",
                           "server auth",],
            }
        }
    }


    with ClosedTempFile() as FileCSR:
        with open(FileCSR, 'w') as f:
            json.dump(PayloadCSR, f)

        pipe2(("cfssl", "genkey", FileCSR),
              ("cfssljson", "-bare", args.Output))

    with ClosedTempFile() as FileConfig:
        with open(FileConfig, 'w') as f:
            json.dump(PayloadConfig, f)

        pipe2(("cfssl", "sign", "-ca", args.CAName + ".pem",
               "-ca-key", args.CAName + "-key.pem", "-config", FileConfig,
               "-profile", "default", args.Output + ".csr"),
              ("cfssljson", "-bare", args.Output))

def cmdInfo(args):
    subp.call(["openssl", "x509", "-in", args.Cert, "-text", "-noout"])

def main():
    import argparse

    Parser = argparse.ArgumentParser(description='PKI workflow.')

    SubParsers = Parser.add_subparsers(metavar="CMD", help='Commands')

    ParserBuildCA = SubParsers.add_parser(
        'build-ca', help='Build a CA from scratch, with a intermediate CA.')
    ParserBuildCA.add_argument('-r', "--root", type=str, dest="Root",
                               help='Base filename of the root CA.')
    ParserBuildCA.add_argument('-i', "--intermediate", type=str, dest="Inter",
                               help='Base filename of the intermediate CA.')
    ParserBuildCA.add_argument('-o', "--output-bundle", type=str, dest="Output",
                               default="bundle-ca.pem",
                               help='Filename of the result CA bundle. '
                               'Default: %(default)s')
    ParserBuildCA.set_defaults(func=cmdBuildCA)

    ParserNewCert = SubParsers.add_parser(
        'new-cert', help='Create and sign a new certificate.')
    ParserNewCert.add_argument("Output", type=str,
                               help="Base filename of the new certificate.")
    ParserNewCert.add_argument("-a", "--ca", type=str, dest="CAName",
                               help="Base filename of the signing CA.")
    ParserNewCert.add_argument('-c', "--common-name", type=str, dest="CommonName",
                               help="Common name of the new certificate.")
    ParserNewCert.add_argument('-H', "--hosts", type=str, dest="Hosts",
                               nargs='+',
                               help="Hostnames or IPs associated with the new "
                               "certificate.")
    ParserNewCert.set_defaults(func=cmdNewCert)

    ParserInfo = SubParsers.add_parser(
        'info', help='Show info of a certificate.')
    ParserInfo.add_argument("Cert", type=str, metavar="FILE",
                               help="The file to show info of.")
    ParserInfo.set_defaults(func=cmdInfo)

    Args = Parser.parse_args()

    Args.func(Args)

if __name__ == "__main__":
    main()

Reference