应用场景:leader需要做个监控程序扫描自己网站的服务器证书状态是否符合设置,出现异常则发短信/Email给管理人员。
假设我们要监控的URL是https://baidu.com,首先使用URL建立https通道,连接正常则服务器会发送证书回客户端。
JAVA需要用到的jar包,bouncy castle相关的包,大家自行下载。maven配置:
org.bouncycastle
bcprov-jdk15on
${bouncycastle.version}
org.bouncycastle
bcpkix-jdk15on
${bouncycastle.version}
org.bouncycastle
bcmail-jdk15on
${bouncycastle.version}
org.bouncycastle
bctls-jdk15on
${bouncycastle.version}
org.bouncycastle
bcprov-ext-jdk15on
${bouncycastle.version}
org.bouncycastle
bcpg-jdk15on
${bouncycastle.version}
获取服务器证书代码如下:
public List getServerCertificates(String url) {
try {
// http转https
if (url.startsWith("http:")) {
url = url.replace("http", "https");
} else if (url.indexOf("http") < 0) {
url = "https://" + url;
}
// enableTLSChainTesting(false);
ArrayList list = new ArrayList();
URL urlConnect = new URL(url);
HttpsURLConnection conn = (HttpsURLConnection) urlConnect.openConnection();
// 设置请求基本属性
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
//创建sslsocket通道
conn.setSSLSocketFactory(createSSLFactory());
conn.connect();
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
Certificate[] certs = conn.getServerCertificates();
for (Certificate cert : certs) {
if (cert instanceof X509Certificate) {
list.add((X509Certificate) cert);
} else {
logger.info("Unsupported certificate type. type=" + cert.getClass().getName());
}
}
return list;
}
return null;
} catch (SSLHandshakeException se) {
// 无法验证
throw new MonitorException(se.getMessage(), se.getCause());
} catch (Exception e) {
throw new MonitorException(e.getMessage());
}
}
在上面代码中需要创建SSLSocket,需要有个可信任源来验证服务器证书。如果没有信任源则会产生此类错误,unable to find valid certification path to requested target
下面是创建信任源以及返回sslsocket的两种方式:
public SSLSocketFactory createSSLFactory() {
try {
//此类型自己创建一个类实现TrustManager亦可在实现类里判断
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, new TrustManager[] { new TrustAllX509TrustManager() }, new java.security.SecureRandom());
return sc.getSocketFactory();
/*
//此类型自己创建jks文件导入信任证书
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream is =
getClass().getClassLoader().getResourceAsStream("config/webtrust.jks");
keystore.load(is, null);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keystore);
SSLContext ctx = SSLContext.getInstance("SSL");
ctx.init(null, tmf.getTrustManagers(), null);
SSLSocketFactory sf =ctx.getSocketFactory();
return sf;
*/
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
我这里业务需求需要信任所有证书,否则服务器证书过期,则获取不到certificates,出现握手错误。
TrustAllX509TrustManager.java如下:
class TrustAllX509TrustManager implements X509TrustManager {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(java.security.cert.X509Certificate[] certs,
String authType) {
}
public void checkServerTrusted(java.security.cert.X509Certificate[] certs,
String authType) {
}
}
到目前为止,则可以获取服务器的证书了。
验证证书吊销状态在CA机构的CRL文件可以验证,但是想要获取证书的即时状态就需要访问CA机构的OCSP服务了。
CA机构的OCSP服务地址一般都是在证书上面的,如图:
那我们怎么通过代码获取到该扩展项并且去请求OCSP服务地址来验证证书呢?
首先将获取回来的证书强转到X509Certificate格式,然后获取证书的der格式证书,然后再获取扩展项
使用扩展项可以获取到颁发者机构信息,再根据特定的OID获取URL。代码如下:
TBSCertificate tbs = TBSCertificate.getInstance(cert.getTBSCertificate());
Extensions extensions = tbs.getExtensions();
AuthorityInformationAccess access = AuthorityInformationAccess.fromExtensions(extensions);
AccessDescription[] descs = access.getAccessDescriptions();
for (AccessDescription desc : descs) {
if (desc.getAccessMethod().getId().equalsIgnoreCase("1.3.6.1.5.5.7.48.1")) {
ocspUrl = desc.getAccessLocation().getName().toString();
}
}
到这里就能获取到证书上面CA机构的OCSP服务器地址。
还需要创建OCSP request去请求,所有CA机构的OCSP服务器都是按照RCF6960协议来进行定义的,需要了解的朋友可以看下
https://www.rfc-editor.org/rfc/pdfrfc/rfc6960.txt.pdf
具体可以参考4.1.1的ocsp request的参数结构。
public static OCSPReq buildOCSPRequest(final X509Certificate x509Certificate, final X509Certificate issuerX509Certificate) {
try {
CertificateID certId = new CertificateID(
(new BcDigestCalculatorProvider())
.get(CertificateID.HASH_SHA1),
new X509CertificateHolder(issuerX509Certificate.getEncoded()),
x509Certificate.getSerialNumber());
final OCSPReqBuilder ocspReqBuilder = new OCSPReqBuilder();
ocspReqBuilder.addRequest(certId);
final OCSPReq ocspReq = ocspReqBuilder.build();
return ocspReq;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
贴下我这边的请求创建,对这块的了解也是初始阶段,有熟悉的老铁可以交流交流。
具体的请求:
使用http协议和get请求,将得到的ocsp request进行URLDecode,具体的意思英文不好的同学自己google翻译下。请求服务器返回的流反序列化成OCSPResp对象。这个对象是在bouncycastle包里面的。
下面是对OCSPResp对象的解析操作。判断证书处于哪个状态。
if (OCSPResp.SUCCESSFUL == ocspResp.getStatus()) {
// 连接成功
BasicOCSPResp basic = (BasicOCSPResp) ocspResp.getResponseObject();
SingleResp[] resps = basic.getResponses();
if (resps != null && resps.length == 1) {
SingleResp resp = resps[0];
CertificateStatus certStatus = resp.getCertStatus();
if (certStatus == CertificateStatus.GOOD) {
// 证书正常状态
gdcaCertResp.setStatus(OcspResp.CERT_STATUS_GOOD);
} else {
if (certStatus instanceof RevokedStatus) {
// 证书吊销
gdcaCertResp.setStatus(OcspResp.CERT_STATUS_REVOKED);
gdcaCertResp.setRevokeTime(((RevokedStatus) certStatus).getRevocationTime());
logger.warn("The Certificate was revoked!revokedTime:{}", gdcaCertResp.getRevokeTime());
} else if (certStatus instanceof UnknownStatus) {
gdcaCertResp.setStatus(OcspResp.CERT_STATUS_UNKNOWN);
}
}
}
}
到这里就完成了获取服务器证书和验证服务器证书的操作了。