先说一下我的环境,Python是2.7.13版本的,Python我用的话一直都用2.7版本的,没想到子版本里面也还有不同的地方。
接下来就说一说我这几天遇见的问题,Python ssl双向认证的问题;
建立ssl的安全socket链接,ssl这个介绍的网上一抓一大把,Python来写的也是一抓一大把(客户端的、服务端的),我就不多说了。
我只说几个需要注意的点:
(1)协议是一个坑,客户端和服务端的协议要对应,要不然会报错;
(2)证书一个坑,作为ssl来讲证书在里面扮演着一个很重要的角色,主要有这么几个证书:
CA证书(自签名的证书,也是根证书,是证书信任链的起点,用来颁发证书),它的作用就是负责校验对端传过来证书合法性的;
Server公钥证书,用来发给客户端表明身份的;
Server私钥证书,给自己用的,用来协商秘钥之类的。
Client公钥证书(非必要);
Client私钥证书(非必要)。
另外不同的开发环境证书的存储方式和编码格式也是不一样的,比如说java里面有一个用keytool生成的jks格式的证书库,Android里面有一个也是用keytool生成的叫做bks格式的证书库,这个网上也有很多资料我就不啰嗦了。
那么在Python里面它是要求要用PEM格式进行编码的后缀名无所谓(.crt/.cer/.pem/.gousheng都可以),但它的格式是这样的:
证书格式
Bag Attributes
friendlyName: CN=,OU=,O=,L=,ST=**,C=cn -storepass xxxx -keypass xxxx
localKeyID: … …
subject=/C=cn -storepass xxxx -keypass xxxx/ST=xxxx/L=xxxx/O=xxxx/OU=xxxx/CN=localhost
issuer=/C=cn -storepass xxxx -keypass xxxx/ST=xxxx/L=xxxx/O=xxxx/OU=xxxx/CN=localhost
—–BEGIN CERTIFICATE—–
… ….
—–END CERTIFICATE—–
key格式
Bag Attributes
friendlyName: *
localKeyID: … ….
Key Attributes:
—–BEGIN ENCRYPTED PRIVATE KEY—–
… …
—–END ENCRYPTED PRIVATE KEY—–
这两个是我从Android bks里面扒出来的,它主要的是在BEGIN和END之间的部分。
接下来我就说是怎么被坑的:
我的对端,也就是服务端是用java写的,它用的是keystore模式。
最开始我的代码是这样的:
mContext = ssl.SSLContext(ssl.PROTOCOL_TLSv1) #指定ssl版本,为tls1
mContext.verify_mode = ssl.CERT_REQUIRED #指定证书校验模式,需要交验
mContext.check_hostname = true #检查主机名
mContext.load_verify_locations("https.crt") #加载校验的根证书
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)#建立普通socket
mContext.wrap_socket(s, server_hostname='10.8.40.116') #封装一层,建立ssl socket
ssl_sock.connect(('10.8.40.116', 9999)) #链接
这个SSLContext 有的小伙伴们可能没有,不过别慌是可以手动添加的。也很简洁,然后我就运行啊,然后就报错 : 证书校验失败。
后来我发现这个https.crt就他么的不是根证书,那怎么办? 别慌,我们可以不进行证书校验,虽然不安全啊:
mContext.verify_mode = ssl.CERT_NONE #指定证书校验模式,不需要校验
我以为这就成了呢,别慌后面还有坑呢。
改了这一条之后我再运行就报错:ssl握手失败,ssl握手的过程大家自行脑补啊,我这个火大啊,果断用wireshark抓包看看,之后我发现服务器那边已经把公钥证书传过来,我也没校验它怎么会失败呢?结果我返回来仔细的研究了一下之前java Client端我发现,他么的还是个双向认证!
SSLContext clientContext = SSLContext.getInstance("TLS");
//第一个参数 身份验证密钥源,第二参数 信任决策源, 第三个参数 随机数源
clientContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(),null);
以后我们看java的ssl链接不用多看,就看它SSLContext初始化的时候传入的参数,如果前两个都有说明是双向的,只有第一个,说明是服务端,只有第二个说明是客户端。
好吧,那我也双向 认证吧,但是问题来了,以前java客户端的证书和key的存储格式是Android.bks 秘钥和证书存一起了,我要是用Python的话先要把它们导出来,怎么办呢?我最后在网上找了一招:
import java.io.FileOutputStream;
import java.security.Key;
import java.security.KeyStore;
import java.security.Security;
import java.util.Enumeration;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
public class offset
{
/**
* 从BKS格式转换为PKCS12格式
*
* @param jksFilePath String JKS格式证书库路径
* @param jksPasswd String JKS格式证书库密码
* @param pfxFilePath String PKCS12格式证书库保存文件夹
* @param pfxPasswd String PKCS12格式证书库密码
*/
public static void covertBKSToPFX(String jksFilePath, String jksPasswd,
String pfxFolderPath, String pfxPasswd) throws Throwable
{
FileInputStream fis = null;
try
{
KeyStore inputKeyStore = KeyStore.getInstance("BKS",new org.bouncycastle.jce.provider.BouncyCastleProvider());
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
fis = new FileInputStream(jksFilePath);
char[] srcPwd = jksPasswd == null ? null : jksPasswd.toCharArray();
char[] destPwd = pfxPasswd == null ? null : pfxPasswd.toCharArray();
inputKeyStore.load(fis, srcPwd);
KeyStore outputKeyStore = KeyStore.getInstance("PKCS12");
Enumeration<String> enums = inputKeyStore.aliases();
while (enums.hasMoreElements())
{
String keyAlias = (String) enums.nextElement();
System.out.println("alias=[" + keyAlias + "]");
outputKeyStore.load(null, destPwd);
if (inputKeyStore.isKeyEntry(keyAlias))
{
Key key = inputKeyStore.getKey(keyAlias, srcPwd);
java.security.cert.Certificate[] certChain = inputKeyStore.getCertificateChain(keyAlias);
outputKeyStore.setKeyEntry(keyAlias, key, destPwd, certChain);
}
String fName = pfxFolderPath + "_" + keyAlias + ".pfx";
FileOutputStream out = new FileOutputStream(fName);
outputKeyStore.store(out, destPwd);
out.close();
outputKeyStore.deleteEntry(keyAlias);
}
}
finally
{
try
{
if (fis != null)
{
fis.close();
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
public static void main(String[] args)throws Exception
{
try {
covertBKSToPFX("G:\\ftp-share\\wangzhipeng\\android.bks", "123456", "G:\\ftp-share\\wangzhipeng\\", "123456");
} catch (Throwable e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
这个可以把.bks转化成.pkf(#pkcs 12格式),org.bouncycastle.jar包这个自己在网上可以下载到,自己手动导入进去。
然后,用openssl 把.pkf拆成证书和key:
openssl pkcs12 -in test.pfx -clcerts -nokeys -out cert.pem 可能需要密码:这里的密码就是.pkf的密码。
openssl pkcs12 -in test.pfx -nocerts -out key.pem 生成key.pem
最后代码就变成了
import ssl,socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ssl_sock = ssl.wrap_socket(s, certfile = 'cert.pem',keyfile = 'key.pem',ssl_version=ssl.PROTOCOL_TLSv1_2 ) #默认不对服务端证书进行校验,携带上自己的证书和key
ssl_sock.connect(('10.8.40.116', 9999))
就这么三行代码花了我三天的时间去解决它,这不是日乐购了。