为gRPC添加上证书(下)

170 阅读4分钟

重新制作证书

Go1.15+废弃 Common Name 字段,推荐使用SAN证书,不然在连接gRPC服务实会出现错误

rpc error: code = Unavailable desc = connection error: desc = \
  "transport: authentication handshake failed: \
   x509: certificate relies on legacy Common Name field, \
   use SANs or temporarily enable Common Name matching with GODEBUG=x509ignoreCN=0"

上一节已经熟悉签发流程:生成密钥对(.key) => 生成请求(.csr) => CA签发证书(.pem),这次简化配置文件并重新制作CA证书,客户端证书

CA证书

生成CA密钥对

openssl genrsa -out ca.key.pem 4096

生成CA证书

自签证书
openssl req -config openssl-ca.conf \
  -key ca.key.pem \
  -new -x509 -days 3650 \
  -out ca.cert.pem

-----
Country Name (2 letter code) [CN]:
State or Province Name [Guangdong]:
Locality Name [Guangzhou]:
Organization Name [CA]:
Organizational Unit Name []:
Common Name []:
Email Address []:
CA配置文件openssl-ca.conf
base_dir        = .
certificate     = $base_dir/ca.cert.pem # The CA certificate
private_key     = $base_dir/ca.key.pem  # The CA private key
new_certs_dir   = $base_dir             # Location for new certs after signing
database        = $base_dir/index.txt   # Database index file
serial          = $base_dir/serial      # The current serial number

unique_subject  = no                    # Set to 'no' to allow creation of
                                        # several certificates with same subject.

HOME            = .
RANDFILE        = $ENV::HOME/.rnd

####################################################################

[ ca ]
# `man ca`
default_ca       = CA_default      # The default ca section

[ CA_default ]
crl_extensions   = crl_ext
default_crl_days = 30              # How long before next CRL

default_days     = 3650            # How long to certify for
default_md       = sha256          # Use public key default MD
preserve         = no              # Keep passed DN ordering

x509_extensions  = ca_ext          # Extension to add when the -x509 option is used.
policy           = policy_anything

email_in_dn      = no              # Don't concat the email in the DN
copy_extensions  = copy            # Required to copy SANs from CSR to cert

####################################################################

[ req ]
default_bits                    = 2048
default_keyfile                 = ca_key.pem
distinguished_name              = ca_distinguished_name
x509_extensions                 = ca_extensions
string_mask                     = utf8only

[ ca_distinguished_name ]
# See <https://en.wikipedia.org/wiki/Certificate_signing_request>.
countryName                     = Country Name (2 letter code)
stateOrProvinceName             = State or Province Name
localityName                    = Locality Name
organizationName                = Organization Name
organizationalUnitName          = Organizational Unit Name
commonName                      = Common Name
emailAddress                    = Email Address

# Optionally, specify some defaults.
countryName_default             = CN
stateOrProvinceName_default     = Guangdong
localityName_default            = Guangzhou
organizationName_default        = CA
organizationalUnitName_default  =
emailAddress_default            =

####################################################################

[ policy_anything ]
# See the POLICY FORMAT section of the `ca` man page.
countryName            = optional
stateOrProvinceName    = optional
localityName           = optional
organizationName       = optional
organizationalUnitName = optional
commonName             = supplied
emailAddress           = optional

####################################################################

[ ca_ext ]
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always, issuer
basicConstraints       = critical, CA:true
keyUsage               = keyCertSign, cRLSign

[ server_ext ]
# Extensions for server certificates (`man x509v3_config`).
basicConstraints       = CA:FALSE
nsCertType             = server
nsComment              = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage               = critical, digitalSignature, keyEncipherment
extendedKeyUsage       = serverAuth

[ client_ext ]
# Extensions for client certificates (`man x509v3_config`).
basicConstraints       = CA:FALSE
nsCertType             = client, email
nsComment              = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer
keyUsage               = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage       = clientAuth, emailProtection

[ crl_ext ]
# Extension for CRLs (`man x509v3_config`).
authorityKeyIdentifier = keyid:always

服务端证书

生成服务端密钥对

openssl genrsa -out server.key.pem 2048

配置文件openssl.conf

[ req ]
default_bits       = 2048
distinguished_name = req_distinguished_name
req_extensions     = v3_req

[ req_distinguished_name ]
# See <https://en.wikipedia.org/wiki/Certificate_signing_request>.
countryName                    = Country Name (2 letter code)
stateOrProvinceName            = State or Province Name
localityName                   = Locality Name
organizationName               = Organization Name
organizationalUnitName         = Organizational Unit Name
commonName                     = Common Name
emailAddress                   = Email Address

# Optionally, specify some defaults.
countryName_default            = CN
stateOrProvinceName_default    = Guangdong
localityName_default           = Guangzhou
organizationName_default       = CA
organizationalUnitName_default =
commonName_default             = localhost
emailAddress_default           =

[v3_req]
basicConstraints = CA:FALSE
keyUsage         = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName   = @alt_names

[alt_names]
DNS.1 = localhost
IP.1  = 0.0.0.0
IP.2  = 127.0.0.1

生成CSR

openssl req -config openssl.conf \
  -key server.key.pem \
  -new -out server.csr.pem

CA签发服务端证书

openssl ca -config openssl-ca.conf \
  -extensions server_ext -days 2920 \
  -in server.csr.pem \
  -out server.cert.pem

客户端证书

生成客户端密钥对

openssl genrsa -out client.key.pem 2048

生成CSR

openssl req -config openssl.conf \
  -key client.key.pem \
  -new -out client.csr.pem 

CA签发客户端证书

openssl ca -config openssl-ca.conf \
  -extensions client_ext -days 2920 \
  -in client.csr.pem \
  -out client.cert.pem

为gRPC开启安全连接

开启安全验证分为

  • insecure:不使用安全验证

  • server-side:客户端认证服务端

  • mutual:不仅是客户端认证服务端,服务端也认证客户端

上面生成的证书文件目录如下

.
├── 01.pem
├── 02.pem
├── ca.cert.pem           # CA自签证书
├── ca.key.pem
├── client.cert.pem       # 客户端证书
├── client.csr.pem
├── client.key.pem        # 客户端密钥对(需自行保密)
├── index.txt
├── index.txt.attr
├── index.txt.attr.old
├── index.txt.old
├── openssl-ca.conf
├── openssl.conf
├── serial
├── serial.old
├── server.cert.pem       # 服务端证书
├── server.csr.pem
└── server.key.pem        # 服务端密钥对(需自行保密)

将用到的证书和密钥对整理一下并复制到工程目录

......
├── configs
│   └── certs
│       ├── ca.cert.pem
│       ├── client.cert.pem
│       ├── client.key.pem
│       ├── server.cert.pem
│       └── server.key.pem
......

测试一下

pkg 目录下新建 tls.go

└── pkg
    └── secure
        └── tls.go

tls.go :封装密钥对及相关证书的加载过程

package secure

import (
    "crypto/tls"
    "crypto/x509"
    "fmt"
    "os"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
)

func TLSServerOption(certFile, keyFile string) (grpc.ServerOption, error) {
    creds, err := credentials.NewServerTLSFromFile(certFile, keyFile)
    if err != nil {
        return nil, fmt.Errorf("credentials.NewServerTLSFromFile err: %w", err)
    }

    return grpc.Creds(creds), nil
}

func TLSDialOption(certFile, serverName string) (grpc.DialOption, error) {
    creds, err := credentials.NewClientTLSFromFile(certFile, serverName)
    if err != nil {
        return nil, fmt.Errorf("credentials.NewClientTLSFromFile err: %w", err)
    }

    return grpc.WithTransportCredentials(creds), nil
}

func CAServerOption(certFile, keyFile, caFile string) (grpc.ServerOption, error) {
    cert, err := tls.LoadX509KeyPair(certFile, keyFile)
    if err != nil {
        return nil, fmt.Errorf("tls.LoadX509KeyPair err: %w", err)
    }

    certPool := x509.NewCertPool()
    ca, err := os.ReadFile(caFile)
    if err != nil {
        return nil, fmt.Errorf("ioutil.ReadFile err: %w", err)
    }

    if ok := certPool.AppendCertsFromPEM(ca); !ok {
        return nil, fmt.Errorf("certPool.AppendCertsFromPEM err")
    }

    creds := credentials.NewTLS(&tls.Config{
        Certificates: []tls.Certificate{cert},
        ClientAuth:   tls.RequireAndVerifyClientCert,
        ClientCAs:    certPool,
    })

    return grpc.Creds(creds), nil
}

func CADialOption(certFile, keyFile, caFile, serverName string) (grpc.DialOption, error) {
    cert, err := tls.LoadX509KeyPair(certFile, keyFile)
    if err != nil {
        return nil, fmt.Errorf("tls.LoadX509KeyPair err: %w", err)
    }

    certPool := x509.NewCertPool()
    ca, err := os.ReadFile(caFile)
    if err != nil {
        return nil, fmt.Errorf("ioutil.ReadFile err: %w", err)
    }

    if ok := certPool.AppendCertsFromPEM(ca); !ok {
        return nil, fmt.Errorf("certPool.AppendCertsFromPEM err")
    }

    creds := credentials.NewTLS(&tls.Config{
        Certificates: []tls.Certificate{cert},
        ServerName:   serverName,
        RootCAs:      certPool,
    })

    return grpc.WithTransportCredentials(creds), nil
}

最后修改 gRPC 服务的启动代码

package server

import (
    "net"

    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"

    pb "go-web/api/gen/v1"
    "go-web/configs"
    "go-web/internal/app/health/service"
    "go-web/pkg/secure"
)

const (
    GRPC_ADDR = ":58081"
    PATH_SVR  = "certs/server.cert.pem"
    PATH_KEY  = "certs/server.key.pem"
)

func NewGRPC(srv *service.EchoService) (grpcSvr *grpc.Server, err error) {
    credsServerOption, err := secure.TLSServerOption(configs.Path(PATH_SVR), configs.Path(PATH_KEY))
    if err != nil {
        return nil, err
    }
    opts := []grpc.ServerOption{
        // Enable TLS for all incoming connections.
        credsServerOption,
    }

    grpcSvr = grpc.NewServer(opts...)
    pb.RegisterEchoServiceServer(grpcSvr, srv)
    reflection.Register(grpcSvr)

    go func() {
        listener, err := net.Listen("tcp", GRPC_ADDR)
        if err != nil {
            panic(err)
        }
        if err = grpcSvr.Serve(listener); err != nil {
            panic(err)
        }
    }()

    return
}

使用 grpcurl 测试,未加载CA证书验证服务端证书

grpcurl -d '{"message": "Hello, World!"}' localhost:58081 api.v1.EchoService.Echo

Failed to dial target host "localhost:58081": \
  tls: failed to verify certificate: x509: certificate signed by unknown authority

加载CA证书验证服务端证书

grpcurl -cacert ca.cert.pem -d '{"message": "Hello, World!"}' \
  localhost:58081 api.v1.EchoService.Echo

{
  "message": "Hello, World!"
}