yeziruo 发布的文章

前言

本文主要介绍Golang Gin框架中全局处理错误的方法,水一下文章。

使用嵌套

就是在路由函数外面套层壳,侵入性大,不推荐这种处理方式。

type handle func(c *gin.Context) error

func errorHandle(f handle) func(c *gin.Context) {
    return func(c *gin.Context) {
        err := f(c)
        if err != nil {
            c.JSON(http.StatusInternalServerError, err.Error())
        }
    }
}

使用:

// 套在原路由函数,有多少个套多少次
router.GET("/b", errorHandle(func(c *gin.Context) error {
    return fmt.Errorf("1145141919810")
}))

使用panicrecover

Gin带有一个gin.Recovery中间件,用于捕获panic,说明我们可以通过中间件的方式捕获错误,而且panicrecover是原生的,非常方便,但只能捕获同一协程下的错误。

自定义错误接口:

package custom

var ErrorMessage map[int]string

type RequestError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Stack   string `json:"stack,omitempty"`
}

func (err *RequestError) Error() string {
    return err.Message
}

func NewRequestError(code int, err ...error) *RequestError {
    e := new(RequestError)
    e.Code = code
    if v, ok := ErrorMessage[code]; ok == true {
        e.Message = v
    } else {
        e.Message = "server error"
    }
    // runtime.Stack()
    if len(err) >= 1 {
        for i := range err {
            e.Stack += err[i].Error()
            e.Stack += "\n"
        }
    }
    return e
}

func init() {
    ErrorMessage = make(map[int]string)
    // 以此类推定义错误信息
    ErrorMessage[401] = "Unauthorized"
}

自定义中间件:

package custom

import (
    "github.com/gin-gonic/gin"
    "net/http"
)

func ErrorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                // 类型断言,因为 panic 是个 any ,什么都可以丢
                if v, ok := err.(*RequestError); ok == true {
                    c.JSON(v.Code, v)
                } else {
                    c.String(http.StatusInternalServerError, "Internal Server Error")
                }
                c.Abort()
            }
        }()
        // 传递下一棒
        c.Next()
    }
}

使用:

router.Use(custom.ErrorHandler())

router.GET("/a", func(c *gin.Context) {
    // 抛个错误
    panic(custom.NewRequestError(401))
    c.String(http.StatusOK, "hello")
})

15154424.png

前言

本文不是Lego使用教程。

由于SSL证书一年也就申请给几次,所以一般我都是手动去配置申请,上次介绍了Certbot提供acme模块用法,本文将介绍lego这个ACME客户端所提供的的库的用法。基本上重新造了个轮子,没有去用内部具体的接口。

实现

package main

import (
    "crypto"
    "crypto/x509"
    "encoding/pem"
    "github.com/go-acme/lego/v4/certcrypto"
    "github.com/go-acme/lego/v4/certificate"
    "github.com/go-acme/lego/v4/challenge/dns01"
    "github.com/go-acme/lego/v4/lego"
    "github.com/go-acme/lego/v4/log"
    "github.com/go-acme/lego/v4/registration"
    "io/ioutil"
    "time"
)

// 参见: https://pkg.go.dev/github.com/go-acme/lego/[email protected]/registration#User
type AcmeUser struct {
    Email        string
    Registration *registration.Resource
    key          crypto.PrivateKey
}

func (u *AcmeUser) GetEmail() string {
    return u.Email
}

func (u *AcmeUser) GetRegistration() *registration.Resource {
    return u.Registration
}
func (u *AcmeUser) GetPrivateKey() crypto.PrivateKey {
    return u.key
}

// 实现 Provider 接口
// 参见: https://pkg.go.dev/github.com/go-acme/lego/v4/[email protected]#Provider
// lego本身提供一些DNS服务商的接口,但我一般都是手动申请
type plainDnsProvider struct{}

func (p *plainDnsProvider) Present(domain, token, keyAuth string) error {
    log.Infof("domain: _acme-challenge.%s token: %s", domain, token)
    return nil
}

func (p *plainDnsProvider) CleanUp(domain, token, keyAuth string) error {
    return nil
}

func main() {
    // 本地ACME测试用
    // httpClient := &http.Client{
    //     Transport: &http.Transport{
    //         TLSClientConfig: &tls.Config{InsecureSkipVerify: true
    //     },
    // }}

    key, err := ioutil.ReadFile("account_private.key")
    if err != nil {
        panic(err)
    }
    block, _ := pem.Decode(key)
    priKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
    if err != nil {
        panic(err)
    }

    // 加载账户信息
    user := AcmeUser{
        Email: "[email protected]",
        key:   priKey,
    }
    config := lego.NewConfig(&user)
    // Pebble 默认地址
    // config.CADirURL = "https://127.0.0.1:14000/dir"
    // config.CADirURL = "https://api.buypass.com/acme/directory"
    config.CADirURL = "https://acme-v02.api.letsencrypt.org/directory"
    config.UserAgent = "acm_go/0.0.1"
    // 秘钥类型
    config.Certificate.KeyType = certcrypto.RSA2048
    // 本地测试用
    // config.HTTPClient = httpClient

    // 新建客户端
    client, err := lego.NewClient(config)
    if err != nil {
        panic(err)
    }

    // 账户注册
    reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
    // 使用秘钥查找账户
    // reg, err := client.Registration.ResolveAccountByKey()
    if err != nil {
        panic(err)
    }
    user.Registration = reg

    // 生成证书请求
    request := certificate.ObtainRequest{
        Domains: []string{"yajyusenpai.coat.co.jp"},
        // 证书链
        Bundle: true,
    }

    dnsProvider := plainDnsProvider{}
    // dns.NewDNSChallengeProviderByName(provider_name)
    // dnspodConfig := dnspod.Config{config...}
    // dnspodProvider := dnspod.NewDNSProviderConfig(&dnspodConfig)

    // 以DNS方式验证
    err = client.Challenge.SetDNS01Provider(&dnsProvider, dns01.AddDNSTimeout(6*time.Minute))
    if err != nil {
        panic(err)
    }

    // 自动轮询
    certificates, err := client.Certificate.Obtain(request)
    if err != nil {
        panic(err)
    }

    // 导出
    _ = ioutil.WriteFile("certificate.crt", certificates.Certificate, 0755)
    _ = ioutil.WriteFile("certificate_private.key", certificates.PrivateKey, 0755)
}

220707063204.jpg

前言

STUN Server是一座灯塔,照亮通向你我之间的道路。

本文在Alpine Linux上编译STUNTMAN来搭建STUN Server,简单够用。

安装

# 安装依赖
apk update
apk add openssl-dev boost-dev
# 下载&编译
wget https://www.stunprotocol.org/stunserver-1.2.16.tgz
tar xvf stunserver-1.2.16.tgz
cd stunserver
make

编译完成后有3个文件:

stunclient      客户端
stunserver      服务器
stuntestcode    单元测试

我们只关心服务器的使用,其他可使用--help来查看用法。

./stunserver

结果:

134632.png

结尾

你可以选择更吼的STUN Server,比方说coturn,因为STUNTMAN到目前已经两年没更新了:

https://github.com/coturn/coturn

前言

Golang中没有原生的队列实现,故实现了一个简单的队列,了解队列的工作方式。

互斥锁

互斥锁是并发控制常用的一种锁,保证了同一时刻同一个资源对象只有一个线程或协程访问,避免出现对变量进行拷贝再赋值导致的问题,而在队列中,使用互斥锁能够保证同一时刻只有入队或出队操作,避免出现混乱,简单的用法如下:

package main

import (
    "fmt"
    "sync"
)

func main() {
    // 互斥锁
    var locker sync.Mutex
    // 一个共享变量
    num := 0

    for i := 0; i < 114514; i++ {
        go func() {
            locker.Lock()
            num++
            locker.Unlock()
            fmt.Println(num)
        }()
    }
    for {
        if num >= 114514 {
            break
        }
    }
    fmt.Println("---------")
    fmt.Println(num)
}

可以尝试去除这个互斥锁运行,但你会发现最后num并没有达到114514,所以程序不会自动结束运行。

切片实现

队列的实现方式有很多种,使用链表性能更优,适合先进先出,但我这里使用了切片来实现,实现了入队出队操作,对内置切片进行维护,包括前移和增容操作。

package main

import (
    "fmt"
    "sync"
)

type Queue struct {
    // 内部维护的切片
    queueSlice []interface{}
    // 容量
    cap uint
    // 起始队列位置
    begin uint
    // 队列尾部位置
    end uint
    // 默认队列大小
    defaultLength uint
    // 并发互斥锁
    locker sync.Mutex
}

func QueueInit(defaultLength uint) *Queue {
    return &Queue{
        queueSlice:    make([]interface{}, defaultLength, defaultLength),
        cap:           defaultLength,
        begin:         0,
        end:           0,
        defaultLength: defaultLength,
        locker:        sync.Mutex{},
    }
}

func (q *Queue) Length() uint {
    // 通过队尾队列开始位置计算长度
    q.locker.Lock()
    length := q.end - q.begin
    q.locker.Unlock()
    return length
}

func (q *Queue) Push(value interface{}) {
    q.locker.Lock()
    // 未超出容量
    if q.end < q.cap {
        q.queueSlice[q.end] = value
        q.end++
    } else {
        // 当长度小于容量进行前移
        if q.end-q.begin < q.cap {
                    // 用for移动会更好
                    // copy,append 也是有开销的
            temp := q.queueSlice[q.begin:q.end]
            q.queueSlice = make([]interface{}, q.cap, q.cap)
            copy(q.queueSlice, temp)
            q.begin = 0
            q.end = uint(len(temp))
            q.queueSlice[q.end] = value
            q.end++
        }
        // 长度达到容量对切片进行扩容
        if q.end >= q.cap {
            temp := make([]interface{}, q.end-q.begin)
            copy(temp, q.queueSlice[q.begin:q.end])
                    // 扩增一倍容量
            q.queueSlice = make([]interface{}, q.cap+q.defaultLength, q.cap+q.defaultLength*2)
            q.cap = q.cap + q.defaultLength
            copy(q.queueSlice, temp)
            q.queueSlice[q.end] = value
            q.end++
        }
    }
    // fmt.Println("|-value", value, "cap", q.cap, "begin", q.begin, "end", q.end)
    q.locker.Unlock()
}

func (q *Queue) Get() interface{} {
    q.locker.Lock()
    if q.begin < q.end {
        value := q.queueSlice[q.begin]
        q.begin++
        q.locker.Unlock()
        return value
    }
    q.locker.Unlock()
    return nil
}

func main() {
    q := QueueInit(6)
    go func(queue *Queue) {
        for {
            value := queue.Get()
            if value == nil {
                continue
            }
            fmt.Println("out", value)
        }
    }(q)
    for i := 0; i <= 27; i++ {
        q.Push(i)
        fmt.Println("in", i)
    }
    for {
        if q.Length() == 0 {
            break
        }
    }
    fmt.Println("len", q.Length())
}

链表实现

package main

import (
    // container/list
    "fmt"
)

type Node struct {
    prev  *Node
    next  *Node
    value interface{}
}

type Queue struct {
    length uint
    // 始节点
    begin  *Node
    // 尾节点
    end    *Node
}

func (q *Queue) Push(value interface{}) {
    node := &Node{prev: nil, next: nil, value: value}
    if q.length == 0 {
        q.begin = node
        q.end = node
        q.length++
        return
    }
    q.end.next = node
    node.prev = q.end
    q.end = node
    q.length++
}

func (q *Queue) Get() interface{} {
    if q.length == 0 {
        return nil
    }
    value := q.begin.value
    q.begin = q.begin.next
    q.length--
    return value
}

func NewQueue() *Queue {
    return &Queue{
        length: 0,
        begin:  nil,
        end:    nil,
    }
}

func main() {
    q := NewQueue()
    go func(q *Queue) {
        for {
            value := q.Get()
            if value == nil {
                break
            }
            fmt.Println("out", value)
        }
    }(q)
    for i := 0; i < 100; i++ {
        q.Push(i)
        fmt.Println("in", i)
    }
    for {
        if q.length == 0 {
            break
        }
    }
}

开启telnet

首先登录,超级用户如下:

UserName:CMCCAdmin
PassWord:aDm8H%MdA

之后访问:

http://192.168.1.1/cgi-bin/abcdidfope94e0934jiewru8ew414.cgi

返回200ok即代表成功开启。

连接telnet

账号密码如下:

UserName:yhtcAdmin
PassWord:Cm1@YHfw

终端输入:

tcapi get WLan_Common SSIDPre
tcapi get WLan11ac_Common SSIDPre
tcapi set WLan_Common SSIDPre ''
tcapi set WLan11ac_Common SSIDPre ''
tcapi commit WLan
tcapi commit WLan11ac
tcapi save

重启即可。

18203238.png

18204002.png

参考

PT632电信“老猫”分析:
https://www.anquanke.com/post/id/259503

前言

我常用的网站要手机验证了。

代码

这里使用buypass的证书,最多五个子域名,这个脚本使用DNS验证。

import json
import datetime

from acme import messages
from acme import challenges
from acme import client
from acme import messages

import josepy as jose

from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.serialization import load_pem_private_key

def new_csr(domain):
    key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048,
    )
    with open(domain[0] + ".key", "wb") as f:
        f.write(key.private_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PrivateFormat.TraditionalOpenSSL,
            encryption_algorithm=serialization.NoEncryption(),
        ))
    csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([
        x509.NameAttribute(NameOID.COMMON_NAME, domain[0]),
    ])).add_extension(
        x509.SubjectAlternativeName([x509.DNSName(i) for i in domain]),
        critical=False,
    ).sign(key, hashes.SHA256())
    return csr.public_bytes(serialization.Encoding.PEM)

def main():
    domain = ["your_domain_list"]
    # 生成csr
    csr = new_csr(domain)
    # 加载用户秘钥
    # RSA 2048bits-4096bits
    with open("./private.key", "r") as f:
        key = load_pem_private_key(f.read().encode("utf-8"), password=None)
    # 获取jwk
    acc_key = jose.JWKRSA(key=key)
    net = client.ClientNetwork(acc_key, user_agent="acm/0.0.1")
    # 本地测试用
    # net.verify_ssl = False
    # 取得acme目录
    directory = messages.Directory.from_json(net.get("https://api.buypass.com/acme/directory").json())
    # directory = messages.Directory.from_json(net.get("https://acme-v02.api.letsencrypt.org/directory").json())

    client_acme = client.ClientV2(directory, net=net)

    # 加载账户信息
    # with open("./account.json", "r") as f:
    #     user = messages.RegistrationResource.json_loads(f.read())
    # client_acme.query_registration(user)

    # 首次注册
    regr = client_acme.new_account(
        messages.NewRegistration.from_data(
            email=("[email protected]"),
            terms_of_service_agreed=True,
        )
    )

    # 保存账户信息
    with open("./account.json", "w") as f:
        f.write(json.dumps(regr.to_json()))

    # 新建订单
    order = client_acme.new_order(csr)
    challenges_list = []
    for authz in order.authorizations:
        for i in authz.body.challenges:
            # 选取DNS验证
            if isinstance(i.chall, challenges.DNS01):
                response, validation = i.response_and_validation(client_acme.net.key)
                challenges_list.append([i, response])
                print("[DNS]", "_acme-challenge." + authz.body.identifier.value)
                print("[DNS]", validation)

    while input("ok? (y) ") != "y":
        pass

    # 通知准备验证
    for i in challenges_list:
        client_acme.answer_challenge(i[0], i[1])
    print("[INFO]", "wait...")

    # 自动轮询2分钟并取回证书
    finalized_orderr = client_acme.poll_and_finalize(order, deadline=datetime.datetime.now() + datetime.timedelta(minutes=2))
    print("[OUT]", finalized_orderr.fullchain_pem)
    with open(domain[0] + ".crt", "w") as f:
        f.write(finalized_orderr.fullchain_pem)   
    pass

if __name__ == "__main__":
    main()

JSON Web Key

使用JSON的结构来承载公钥。(RFC7517)

import json
import base64
import binascii
from cryptography.hazmat.primitives.serialization import load_pem_private_key

def b64(text):
    return base64.urlsafe_b64encode(text).decode('utf8').replace("=", "")

def get_jwk(path):
    with open(path, "r") as f:
        pri = load_pem_private_key(f.read().encode("utf-8"), password=None)
    pub = pri.public_key().public_numbers()
    e = "{:x}".format(pub.e)
    e = "0{0}".format(e) if int(e) % 2 else e
    e = b64(binascii.unhexlify(e))
    n = b64(binascii.unhexlify(hex(pub.n)[2:]))
    return {"n": n, "e": e, "kty": "RSA"}

if __name__ == "__main__":
    print(get_jwk("./private.key"))

使用了RSA的模数与指数来代表一份公钥,如果是椭圆则是基点坐标。

前言

接上一篇文章,本文章将讲述一个二/多级CA与OCSP服务器的搭建。

通常的CA架构

通常CA为多级架构,由根CA签署若干个不同用途的子CA,再由子CA签发给客户,以本站的证书为例,它是一个两级CA架构:

105252.png

二级从名字可以看出是专门于签署DV SSL证书的。一般CA机构会颁发三种用途的CA,分别是用于SSL,邮件/文件签名和代码签名。
要实现上面三种CA,首先了解一下扩展密钥用法,扩展密钥用法指定了CA及其所颁发证书的用途,在OpenSSL中有如下用法:

serverAuth             SSL服务器验证
clientAuth             SSL客户端验证
codeSigning            代码签名
emailProtection        邮件加密
timeStamping           可信时间戳
OCSPSigning            OCSP签名
ipsecIKE               密钥交换协议
msCodeInd              Microsoft 个人代码签名
msCodeCom              Microsoft 商业代码签名
msCTLSign              Microsoft 信任列表签名
msEFS                  Microsoft Windows文件系统加密
(RFC 5280)(https://www.openssl.org/docs/manmaster/man5/x509v3_config.html)
题外话:在Windows中,内核驱动必须被微软交叉签名的CA所签署的证书所签署,在早期有被泄露出来的代码签名证书,可以通过个人搭建时间戳服务器来绕过签名的时间戳认证,即让系统认为签署的时间在证书的有效期中,从而实现加载未正儿八经签名的内核驱动。(HT Srl)

在OpenSSL配置文件中指定extendedKeyUsage即可为证书赋予不同的用途。其实这些属性代表的是oid,即对象标识符,除了通用的oid外,还用于证书私有策略。
比方说本站证书中的1.3.6.1.4.1.6449.1.2.2.64,前缀1.3.6.1.4.1.为命名空间,6449代表私营企业编号(PEN,由IANA分配,http://www.iana.org/assignments/enterprise-numbers),这里是查询到的是Sectigo Limited,.1.2.2.64代表私有的数字签名证书颁发策略。

CRLOCSP

为保证证书的有效性,引入了CRLOCSP
CRL证书吊销列表是一个由CA签名的结构化列表文件,包含CA信息,吊销的证书序列号和日期,和下一次更新时间,一般情况下这个文件通过http提供,由操作系统下载导入,并在到达更新时间时更新。
OCSP在线证书状态协议是继CRL后出现的证书状态检查协议,解决了要下载CRL和实时性问题,一般使用http协议承载,返回三个状态:GOOD,REVOKED,UNKNOWN
OCSP虽然解决了大部分CRL所带来的问题,但是OCSP会像DNS一样存在隐私问题,所以一般会让服务器缓存一个最近的OCSP响应来解决,此外,OCSP还容易遭到复读机攻击(重放),解决方法类似于解决CSRF问题一样,由客户端请求时附带一个nonce,让服务器原样返回。

根CA与子CA

构建CA的配置文件大部分均相同,只需要修改一些主题名称,扩展密钥用法和crlocsp地址即可:

[ ca ]
default_ca    = CA_default
[ CA_default ]
dir        = .
certs        = $dir/certs
crl_dir        = $dir/crl
database    = $dir/index.txt
new_certs_dir    = $dir/newcerts

certificate    = $dir/cacert.crt
serial        = $dir/serial
crlnumber    = $dir/crlnumber

crl        = $dir/crl.crl 
private_key    = $dir/cakey.key

x509_extensions    = usr_cert

name_opt     = ca_default
cert_opt     = ca_default

default_days    = 730
default_crl_days= 30
default_md    = default
preserve    = no
policy        = policy_match
copy_extensions = none

#[ crl_info ]
#URI.0 = http://127.0.0.1/root/ca.crl

[ policy_match ]
countryName        = optional
stateOrProvinceName    = optional
organizationName    = optional
organizationalUnitName    = optional
commonName        = optional
emailAddress        = optional

[ req ]
utf8 = yes
default_bits        = 2048
default_keyfile     = privkey.pem
distinguished_name    = req_distinguished_name
x509_extensions    = v3_ca
string_mask = utf8only

[ req_distinguished_name ]
# 默认主题信息
countryName            = Country Name (2 letter code)
countryName_default        = CN
countryName_min            = 2
countryName_max            = 2

stateOrProvinceName        = State or Province Name (full name)
stateOrProvinceName_default    = Beijing

localityName            = Locality Name (eg, city)

0.organizationName        = Organization Name (eg, company)
0.organizationName_default    = Strategic Fooyou Agency

#1.organizationName        = Second Organization Name (eg, company)
#1.organizationName_default    = World Wide Web Pty Ltd

organizationalUnitName        = Organizational Unit Name (eg, section)

commonName            = Common Name (e.g. server FQDN or YOUR name)
commonName_default = Strategic Fooyou Agency Root CA
commonName_max            = 64

emailAddress            = Email Address
emailAddress_max        = 64


[ usr_cert ]
basicConstraints=CA:FALSE
extendedKeyUsage = emailProtection,clientAuth
crlDistributionPoints = URI:http://127.0.0.1/root/ca.crl
authorityInfoAccess = OCSP;URI:http://127.0.0.1/root/ocsp
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer

# Email CA
[ email_ca ]
basicConstraints= critical,CA:true
# 扩展密钥用法
extendedKeyUsage = emailProtection,clientAuth
# CRL
crlDistributionPoints = URI:http://127.0.0.1/root/ca.crl
# OCSP
authorityInfoAccess = OCSP;URI:http://127.0.0.1/root/ocsp
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
keyUsage = cRLSign, keyCertSign

# OCSP证书
[ v3_ocsp ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = OCSPSigning
crlDistributionPoints   = URI:http://127.0.0.1/root/ca.crl
authorityInfoAccess = OCSP;URI:http://127.0.0.1/root/ocsp
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer

# Root CA
[ v3_ca ]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
basicConstraints = critical,CA:true
keyUsage = cRLSign, keyCertSign
crlDistributionPoints = URI:http://127.0.0.1/root/ca.crl #@crl_info
authorityInfoAccess = OCSP;URI:http://127.0.0.1/root/ocsp

CA目录配置:

mkdir rootCA && cd rootCA
mkdir certs
mkdir crl
mkdir newcerts
echo "01" > serial
echo "01" > crlnumber
touch index.txt

自签根CA:

openssl req -new -config rootca.cnf -out ca.csr -keyout cakey.key
openssl ca -selfsign -config rootca.cnf -in ca.csr -out cacert.crt -extensions v3_ca

生成子CA请求:

# 建议新建目录复制和修改配置文件再生成
openssl req -new -config rootca.cnf -out emailca.csr -keyout emailcakey.key -extensions email_ca

签署子CA:

openssl ca -config rootca.cnf -in emailca.csr -out emailca.crt -extensions email_ca

134000.png

用户

同上节所述,将上一节的emailca.crtemailca.key移动到新的子CA目录,按上述步骤配置,以及修改配置文件。

请求文件配置:

[ req ]
default_bits = 2048
encrypt_key = no
utf8 = yes
string_mask = utf8only
req_extensions = v3_req
prompt = yes
distinguished_name = distinguished_name
# SSL SAN
#req_extensions = server_reqext

#[ server_reqext ]
#subjectAltName = @domain_san
#
#[ domain_san ]
#DNS.1 = sfa.org.cn

[distinguished_name]
# 准确来说这里CN应该等于Email,作为友好名称,但我好像没办法让它自动复制
#commonName = Main Domain
#commonName_max = 64
emailAddress = Email Address
emailAddress_max = 64

[ v3_req ]
keyUsage = nonRepudiation, digitalSignature, keyEncipherment

生成签发请求:

openssl req -new -config email.cnf -out certs/email.csr -keyout certs/email.key

由子CA签发证书:

openssl ca -config emailca.cnf -in certs/email.csr -out certs/email.crt -extensions usr_cert -days 365

135208.png

安全性说明:

在签发证书前,要仔细检查(扩展)主题信息,因为OpenSSL没有提供(没查到)编辑主题信息的有效方法。

管理CA

以下提供了简单管理CA的方法。

吊销

openssl ca -config emailca.cnf -revoke certs/email.crt

生成CRL与发布

openssl ca -config emailca.cnf -gencrl -out email.crl [-crldays 1]

之后将这个crl文件放入证书中指定的路径即可,更新时间在配置文件的default_crl_days中已经指定。

响应OCSP

OpenSSL可以作为OCSP服务器使用,在此之前,我们需要签署一个扩展密钥用法为OCSPSigning的证书:

openssl req -new -config rootca.cnf -out ocsp.csr -keyout ocsp.key -extensions v3_ocsp
openssl ca -config rootca.cnf -in ocsp.csr -out ocsp.crt -extensions v3_ocsp

140846.png

开启服务器:

# 复制到新目录
mkdir ocsp && cd ocsp
cp ../emailCA/emailca.crt .
cp ../emailCA/index.txt .
cp ../rootCA/ocsp.crt .
cp ../rootCA/ocsp.key .

# 启动
openssl ocsp -index index.txt -port 80 -rsigner ocsp.crt -rkey ocsp.key -CA emailca.crt -text

验证:

openssl ocsp -issuer chain.crt -cert email.crt -url http://127.0.0.1/

152122.png

(原本在Linux下测试的,后面为了补图又在Windows下测试)

结尾

使用OpenSSL命令行工具告一段落,上述可以用于学习,但不适合与应用集成,现在大多数语言的加密库附带x509功能,可以依托x509来实现颁发管理自动化。

前言

之前写了一篇关于Nginx的客户端验证的文章,让大家使用XCA这个东西,没有讲用OpenSSL来建一个CA,故本文章来讲一讲。
本文章主要介绍搭建一个简单的一级CA,由根证书直接签发最终用户证书。

配置与签发

本次使用的配置文件如下:

[ ca ]
default_ca    = CA_default
[ CA_default ]
# 一些目录配置
dir        = .
certs        = $dir/certs
crl_dir        = $dir/crl
database    = $dir/index.txt
new_certs_dir    = $dir/newcerts

certificate    = $dir/cacert.crt
serial        = $dir/serial
crlnumber    = $dir/crlnumber

crl        = $dir/crl.crl 
private_key    = $dir/cakey.key

# x509证书扩展配置
x509_extensions    = usr_cert

# 证书主题选项
name_opt     = ca_default
cert_opt     = ca_default

# 有效期
default_days    = 730
# 默认crl列表更新时间
default_crl_days= 30
# 默认签名哈希算法
default_md    = default
# 保持DN顺序
preserve    = no
# 证书主题策略
policy        = policy_match
# 不复制来自证书请求文件的扩展
copy_extensions = none

[ crl_info ]
# crl分发点,这个链接必须为URI且指向crl列表
URI.0 = http://ca.coat.jp/root/ca.crl

[ policy_match ]
# 主题均为可选
countryName        = optional
stateOrProvinceName    = optional
organizationName    = optional
organizationalUnitName    = optional
commonName        = optional
emailAddress        = optional

[ req ]
default_bits        = 2048
default_keyfile     = privkey.pem
# 要求填写的字段名DN
distinguished_name    = req_distinguished_name
x509_extensions    = v3_ca
string_mask = utf8only

[ req_distinguished_name ]
countryName            = Country Name (2 letter code)
countryName_default        = JP
countryName_min            = 2
countryName_max            = 2

stateOrProvinceName        = State or Province Name (full name)
stateOrProvinceName_default    = Simokitazawa

localityName            = Locality Name (eg, city)

0.organizationName        = Organization Name (eg, company)
0.organizationName_default    = COAT Inc.

#1.organizationName        = Second Organization Name (eg, company)
#1.organizationName_default    = World Wide Web Pty Ltd

organizationalUnitName        = Organizational Unit Name (eg, section)

commonName            = Common Name (e.g. server FQDN or YOUR name)
commonName_default = COAT Email CA
commonName_max            = 64

emailAddress            = Email Address
emailAddress_max        = 64


[ usr_cert ]
# 基本约束
basicConstraints=CA:FALSE
# 扩展用户用法,这里指定了证书的用途
# 如果你看了之前那片文章并使用了XCA,在它的选项卡中有这些选项,不填写为所有应用程序策略
# https://www.openssl.org/docs/manmaster/man5/x509v3_config.html
extendedKeyUsage = emailProtection,clientAuth
# 配置crl列表
crlDistributionPoints   = @crl_info
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer


[ v3_ca ]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
basicConstraints = critical,CA:true
# 秘钥用法
keyUsage = cRLSign, keyCertSign
extendedKeyUsage = emailProtection,clientAuth
crlDistributionPoints   = @crl_info

配置CA目录:

mkdir emailCA && cd emailCA
mkdir certs
mkdir crl
mkdir newcerts
echo "01" > serial
echo "01" > crlnumber
touch index.txt


首先生成根证书证书请求:

openssl req -new -config openssl.cnf -out ca.csr -keyout cakey.key

自签名根证书:

openssl ca -selfsign -config openssl.cnf -in ca.csr -out cacert.crt -extensions v3_ca

签发后长这样:

0118112027.png

用户证书配置文件:

[ req ]
default_bits = 2048
encrypt_key = no
utf8 = yes
string_mask = utf8only
req_extensions = v3_req
prompt = yes
distinguished_name = distinguished_name

[distinguished_name]
emailAddress = Email Address
emailAddress_max = 64

[ v3_req ]
keyUsage = nonRepudiation, digitalSignature, keyEncipherment

生成用户证书请求:

openssl req -new -config email.cnf -out certs/email.csr -keyout certs/email.key

签发用户证书请求:

openssl ca -config openssl.cnf -in certs/email.csr -out certs/email.crt -extensions usr_cert -days 365

签发后长这样:

1321278785.png

吊销证书:

openssl ca -config openssl.cnf -revoke certs/email.crt

生成crl列表:

openssl ca -config openssl.cnf -gencrl -out ca.crl

结尾

一级CA搭建较为简单,但并不常用,一般都是由根签发二级CA,再由二级CA签发给用户,这种方式方便不同用途的证书管理,同时防止CA被爆破导致签发的证书作废带来的影响。

在下一篇文章将讲述一个二级CAOCSP服务器的搭建。