Python https认证

本文讲述了在Python 2.7.13环境下遇到的SSL双向认证问题,包括协议匹配、证书类型与格式,以及如何处理Java keystore格式的证书。通过实例展示了如何解决证书校验失败和握手失败的问题,提供了将Android .bks转换为Python可用格式的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

先说一下我的环境,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))

就这么三行代码花了我三天的时间去解决它,这不是日乐购了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值