X.509数字证书
X.509是一种数字证书,它使用X.509公钥基础设施标准来验证属于用户、服务或服务器的公钥是否包含在证书中,以及所述用户、服务或服务器的身份。
证书可以由受信任的证书颁发机构签名,也可以是自签名的。
SSL和TLS是最广为人知的协议,它们使用X.509格式。当每次打开浏览器并通过HTTPS访问网页时,它们通常被用来验证服务器的身份。
认证流程
为了保护和验证客户机和服务器之间的通信,它们都需要有有效的证书。当向HTTPS服务发送请求时,客户端将验证该服务是由可信的权威机构认证或基于可信根证书的。在这种情况下,不仅要验证服务器的身份,而且服务器也要验证客户机。
单向认证流程
双向认证(mTLS)流程
主流数字证书的格式
主流Web服务软件
一般来说,主流的Web服务软件,通常都基于OpenSSL和Java两种基础密码库。
- Tomcat、Weblogic、JBoss等Web服务软件,一般使用Java提供的密码库。通过Java Development Kit (JDK)工具包中的Keytool工具,生成Java Keystore(JKS)格式的证书文件。
- Apache、Nginx等Web服务软件,一般使用OpenSSL工具提供的密码库,生成PEM、KEY、CRT等格式的证书文件。
- IBM的Web服务产品,如Websphere、IBM Http Server(IHS)等,一般使用IBM产品自带的iKeyman工具,生成KDB格式的证书文件。
- 微软Windows Server中的Internet Information Services(IIS)服务,使用Windows自带的证书库生成PFX格式的证书文件。
证书格式转换
以下证书格式之间是可以互相转换的。
可以使用JDK中自带的Keytool工具,将JKS格式证书文件转换成PFX格式。也可以将PFX格式证书文件转换成JKS格式。
可以使用OpenSSL工具,将KEY格式密钥文件和CRT格式公钥文件转换成PFX格式证书文件。也可以将PFX格式证书文件转化为KEY格式密钥文件和CRT格式公钥文件。
详细转换命令将在后面介绍。
证书签发
为了与受保护的服务通信,客户机必须做的第一件事是生成私钥和证书签名请求(CSR)。然后将此CSR发送给要签名的证书颁发机构(CA)。在用例中,由服务提供方代表服务器和CA,因为由服务提供方负责管理与服务对话的客户端。对CSR进行签名将生成客户端证书,然后将该证书发送回客户端。
过程中会涉及以下一些密钥或证书文件:
- 根证书:root.crt,在Java体系中叫
truststore
- 根证书密钥:root.key
- 服务器端公钥证书:server.crt
- 服务器端私钥文件:server.key
- 客户端公钥证书:client.crt
- 客户端私钥文件:client.key
- 客户端集成证书(包括公钥和私钥,用于浏览器访问场景):client.p12
过程中会涉及csr
文件,全称为:Certificate Signing Request,证书请求文件的缩写。,也就是证书申请者在申请数字证书时由CSP(加密服务提供者)在生成私钥的同时也生成证书请求文件,证书申请者只要把CSR文件提交给证书颁发机构后,证书颁发机构使用其根证书私钥签名就生成了证书公钥文件,也就是颁发给用户的证书。
如果是Java程序,需要使用keytool
将证书转为JKS格式,JDK11开始建议使用标准PKS#12。
下面是证书生成的内在逻辑示意图:
服务提供商生成自签名根证书
(1)创建根证书私钥:
openssl genrsa -aes256 -out root.key 2048
(2)创建根证书请求文件:
openssl req -new -key root.key -out root.csr
后续参数请自行填写,下面是一个例子:
Country Name (2 letter code) [XX]:CN
State or Province Name (full name) []:Sichuan
Locality Name (eg, city) [Default City]:Chengdu
Organization Name (eg, company) [Default Company Ltd]:Bayconnect
Organizational Unit Name (eg, section) []:ITP
Common Name (eg, your name or your servers hostname) []:root
Email Address []:qiupc@bayconnect.com.cn
A challenge password []:
An optional company name []:
> 可以通过`-utf8 -subj '/C=XX/ST=XX/L=XX/O=XXXX/OU=XX/CN=XX/emailAddress=XX'`的格式直接设置证书的信息
(3)创建根证书:
openssl x509 -req -in root.csr -signkey root.key -CAcreateserial -out root.crt -days 1024 -sha256
服务提供商生成自签名服务器端证书
(1)生成服务器端证书私钥:
openssl genrsa -aes256 -out server.key 2048
(2) 生成服务器证书请求文件,过程和注意事项参考根证书,本节不详述:
openssl req -new -out server.csr -key server.key
(3) 生成服务器端公钥证书
openssl x509 -req -in server.csr -CA root.crt -CAkey root.key -CAcreateserial -out server.crt -days 1024 -sha256
客户端生成证书请求文件
(1)生成客户端证书秘钥:
openssl genrsa -out client.key 2048
(2) 生成客户端证书请求文件,过程和注意事项参考根证书,本节不详述:
openssl req -new -out client.csr -key client.key
亦可使用jkstool
生成证书请求文件
keytool -genkey -alias [$Alias] -keyalg RSA -keysize 2048 -keystore [$Keytool_Path]
keytool -certreq -sigalg SHA256withRSA -alias [$Alias] -keystore [$Keytool_Path] -file [$Keytool_CSR]
服务提供商颁发客户端证书
openssl x509 -req -in client.csr -out client.crt -CA root.crt -CAkey root.key -CAcreateserial -days 1024
或
openssl ca -in client.csr -out client.crt -cert root.crt -keyfile root.key -days 3650
证书格式转换
根证书转Java truststore
JKS
keytool -import -file root.crt -alias rootCA -keystore truststore.jks
服务端证书转PKCS#12
openssl pkcs12 -export -in server.crt -inkey server.key -certfile server.crt -out keystore.p12
证书转PKCS#12
openssl pkcs12 -export -in server.crt -inkey server.key -out keystore.p12
openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.p12
PKCS#12转Java JKS
keytool -importkeystore -srckeystore keystore.p12 -srcstoretype pkcs12 -destkeystore keystore.jks -deststoretype JKS
注意:尽管keytool要求单独输入一个新的JKS的存储密码,但如果输入的JKS存储密码如果与PKCS#12不同,在Java中使用JKS时,出无法读取或校验失败,所以,JKS的存储密码需要与PKCS#12的存储密码相同
服务端配置
以下两种方式根据业务实际需要选择。
方式一:Nginx作为前置Web服务器
如果后端业务不需要标识请求方的身份,或只需要透传证书中的信息,不需要特殊处理,那么可以使用Nginx验证证书的合法性,然后将请求反向代理到后端HTTP服务,配置参考:
server {
listen 443 ssl;
server_name www.example.com;
ssl_certificate server.crt;
ssl_certificate_key server.key;
# 如果server.key有密码,配置key.pass文件
ssl_password_file key.pass
# 根证书
ssl_client_certificate root.crt;
...
}
方式二:Tomcat(Spring Boot)作为Web服务器
如果拿到证书中的客户端信息后有自定义业务处理,那么可以使用Tomcat + Spring Security实现。配置参考:
server:
port: 8443
ssl:
key-store: classpath:config/tls/keystore.jks
key-store-password: changeit
trust-store: classpath:config/tls/truststore.jks
trust-store-password: changeit
client-auth: NEED
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/*
* Enables x509 client authentication.
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.x509()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.NEVER)
.and()
.csrf()
.disable();
// @formatter:on
}
/*
* Create an in-memory authentication manager. We create 1 user (localhost which
* is the CN of the client certificate) which has a role of USER.
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("localhost").password("none").roles("USER");
}
}
使用此种方式时,前置网关需要支持L4级别的反向代理,或使用路由器、iptables等路由手段。
客户端测试和调用
方式一:curl测试HTTPS
curl -ik --cert client.crt --key clientprivate.key "https://localhost:8443/api/gateway/routes"
方式二:Nginx反向代理HTTP到HTTPS
使用Nginx反向代理业务发起的HTTP请求到服务提供商的HTTPS,可以方便的使用Nginx来请求HTTPS和验证其响应,减小业务调用的复杂度,减少流程。所以与HTTPS的操作由Nginx完成,业务调用只考虑HTTP。
server {
listen 8080;
server_name localhost;
location / {
proxy_pass https://registry.bayconnect.com.cn:18443/;
proxy_ssl_certificate /path/to/client.crt;
proxy_ssl_certificate_key /path/to/client.key;
proxy_ssl_protocols TLSv1.2 TLSv1.3;
proxy_ssl_ciphers HIGH:!aNULL:!MD5;
proxy_ssl_trusted_certificate /path/to/root.crt;
# 如果开启了SSL校验,则proxy_pass地址需要使用域名
proxy_ssl_verify on;
proxy_ssl_verify_depth 2;
proxy_redirect off;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Port $http_port;
}
}
方式二:Java测试调用HTTPS
使用Java直接调用HTTPS可以方便的实现自定义校验规则。
public class CertClientDemo {
private final static String PFX_PATH = "/client.p12"; //客户端证书路径
private final static String ROOT_PATH = "/truststore.jks"; //根证书路径
private final static String PFX_PWD = "5Kz3iVHCYIW5io6u"; //客户端证书密码
public static ResponseEntity<String> sslRequestGet() throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException, KeyManagementException, UnrecoverableKeyException {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
InputStream instream = CertClientDemo.class.getResourceAsStream(PFX_PATH);
try {
keyStore.load(instream, PFX_PWD.toCharArray());
} finally {
instream.close();
}
KeyStore rootKeyStore = KeyStore.getInstance("JKS");
InputStream rootKeyInstream = CertClientDemo.class.getResourceAsStream(ROOT_PATH);
try {
rootKeyStore.load(rootKeyInstream, null);
} finally {
rootKeyInstream.close();
}
SSLContext sslContext = SSLContextBuilder.create()
.loadKeyMaterial(keyStore, PFX_PWD.toCharArray())
//.loadTrustMaterial(keyStore, TrustSelfSignedStrategy.INSTANCE)
.loadTrustMaterial(rootKeyStore, TrustSelfSignedStrategy.INSTANCE)
.setProtocol("TLSv1.2")
.build();
HttpClient httpClient = HttpClientBuilder.create()
.setSSLContext(sslContext)
// TODO: 当未使用域名(hostname)访问时,不校验host
//.setHostnameVerifier(new AllowAllHostnameVerifier())
// TODO:设置其它校验规则
.build();
ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
RestTemplate restTemplate = new RestTemplate(requestFactory);
Map<String, String> parameter = new LinkedHashMap<>();
parameter.put("key1", "val1");
parameter.put("key2", "val2");
parameter.put("key3", "val3");
return restTemplate.postForEntity("https://registry.bayconnect.com.cn:8443/httpbin/anything", parameter, String.class);
}
public static void main(String[] args) throws Exception {
System.out.println(sslRequestGet());
}
}
附1:Openssl生成基于ECC算法的密钥
ECC椭圆加密算术相比普通的离散对数计算速度性能要强很多。对于RSA算法来讲,目前至少使用2048位以上的密钥长度才能保证安全性。ECC只需要使用224位长度的密钥就能实现RSA2048位长度的安全强度。在进行相同的模指数运算时速度显然要快很多。
openssl ecparam -name prime256v1 -genkey -out root.key
附2:Openssl生成SM2的密钥
OpenSSL在1.1.1+的版本开始支持SM2。
openssl ecparam -name SM2 -genkey -out root.key