大家在自己假设服务的时候肯定都遇到过这种情况,就是 HTTPS 的 web 界面浏览器不信任,只能手动添加 exception,添加完以后那个小锁还有个叹号 。如何把这个叹号小锁变成小绿锁 呢?这就涉及到自己架设 PKI 颁发证书的问题了。
基本概念
一个 PKI 的结构基本是这样的:系统里有一个根证书(root certificate),存在某个中心服务器(root CA)里。还有一些专门用来给别的服务器签证书的服务器,里面有数个中间证书(intermediate certificate),这些证书在这个 PKI 里是可信的,因为它们都是 root CA 签发的。最后每个 production 服务器上有从这些中间服务器使用中间证书签发的证书。在用户那里,根证书和中间证书都是受到信任的,所以这些 production 服务器的证书也是可信的。这些 production 证书会推导出其它一些密钥,用于通信加密。
具体操作
-
生成 root CA
-
生成 intermediate CA
-
用 root CA 签名 intermediate CA
-
用 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()