由于工作需要,我们开发的 App 需要接入支付宝的支付功能。于是我们着手了解 Alipay 相关的 api 文档。
经过斟酌后,我们选择使用了一个第三方 SDK (https://github.com/fzlee/alipay)。这里不得不吐槽一下官方的python SDK, 从包路径到使用,都带有强烈的 java 风格,没有了 python 的简约气息。
尽管使用了 SDK,但是我们发现,最新的支付宝都使用了公钥证书来签名。而官方 SDK 也只有 java 才支持公钥证书方式,我们使用的第三方 SDK 也没有。 于是乎我们不得不自己来实现公钥证书的解析,但是网络上关于自行实现签名的内容比较少,仅有提供一些说明。
官方提供了自行实现签名的过程 https://docs.open.alipay.com/291/106118。
其中比较关键的是从证书提取app_cert_snalipay_root_cert_sn两个关键参数,这里给出 Java 中的实现:

/**
 * 从公钥证书中提取公钥序列号
 *
 * @param certPath 公钥证书存放路径,例如:/home/admin/cert.crt
 * @return 公钥证书序列号
 * @throws AlipayApiException
 */
public static String getCertSN(String certPath) throws AlipayApiException {
    InputStream inputStream = null;
    try {
        inputStream = new FileInputStream(certPath);
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream);
        MessageDigest md = MessageDigest.getInstance("MD5");
        md.update((cert.getIssuerX500Principal().getName() + cert.getSerialNumber()).getBytes());
        String certSN = new BigInteger(1, md.digest()).toString(16);
        //BigInteger会把0省略掉,需补全至32位
        certSN = fillMD5(certSN);
        return certSN;

    } catch (NoSuchAlgorithmException e) {
        throw new AlipayApiException(e);
    } catch (IOException e) {
        throw new AlipayApiException(e);
    } catch (CertificateException e) {
        throw new AlipayApiException(e);
    } finally {
        try {
            if (inputStream != null) {
                inputStream.close();
            }
        } catch (IOException e) {
            throw new AlipayApiException(e);
        }
    }
}

/**
 * 获取根证书序列号
 *
 * @param rootCertContent
 * @return
 */
public static String getRootCertSN(String rootCertContent) {
    String rootCertSN = null;
    try {
        X509Certificate[] x509Certificates = readPemCertChain(rootCertContent);
        MessageDigest md = MessageDigest.getInstance("MD5");
        for (X509Certificate c : x509Certificates) {
            if (c.getSigAlgOID().startsWith("1.2.840.113549.1.1")) {
                md.update((c.getIssuerX500Principal().getName() + c.getSerialNumber()).getBytes());
                String certSN = new BigInteger(1, md.digest()).toString(16);
                //BigInteger会把0省略掉,需补全至32位
                certSN = fillMD5(certSN);
                if (StringUtils.isEmpty(rootCertSN)) {
                    rootCertSN = certSN;
                } else {
                    rootCertSN = rootCertSN + "_" + certSN;
                }
            }

        }
    } catch (Exception e) {
        AlipayLogger.logBizError(("提取根证书失败"));
    }
    return rootCertSN;

}

private static String fillMD5(String md5) {
    return md5.length() == 32 ? md5 : fillMD5("0" + md5);
}

这里和官网所说的流程大概相同:

第一步中解析X.509证书比较容易,在 python 实现中我们使用了 openssl 来解析证书:

cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)

但是在获取 name 和 serialNumber 碰到了障碍。 在 Java 中我们看到 md.update((c.getIssuerX500Principal().getName() + c.getSerialNumber()).getBytes()); 这一行可以轻松提取 name 和 serialName。 可惜的是,openssl 里只有get_serial_number这样的 API 来提取序列号,没有像 java 里 getIssuerX500Principal 来获取他想要的机构名称。 经过长时间的资料查询和研究,从 https://sbing.vip/archives/2019-new-alipay-php-docking.html 这里找到了线索:

需要拼接成:CN=Ant Financial Certification Authority Class 2 R1,OU=Certification Authority,O=Ant Financial,C=CN

于是找到了解决方法:name = 'CN={},OU={},O={},C={}'.format(certIssue.CN, certIssue.OU, certIssue.O, certIssue.C)
第二步中的拼接和 MD5 校验就比较简单,使用 python 自带的 hashlib 就可以完成,并且比 Java 更简洁。
最后一个问题来自于根证书,源码显示根证书包含多个证书信息,读取文件的时候需要使用 split('\n\n') 来获取证书字符串列表,再遍历获取证书 SN 信息。 还有源码里做了筛选if (c.getSigAlgOID().startsWith("1.2.840.113549.1.1"))Openssl里也没有这样的 API 可以调度。 我没有选择像它那样解析出算法的 OID。我猜想这个就是为了找到指定算法类型,于是我使用了别的方法代替:

try:
    sigAlg = cert.get_signature_algorithm()
except ValueError:
    continue
if b'rsaEncryption' in sigAlg or b'RSAEncryption' in sigAlg:

以上是我对支付宝公钥证书验证的大致理解,最后的算法类型也是我的猜测,有问题可以告诉我哦。这么看起来不是特别困难的问题,但是在解决问题的过程中的确花了很多时间,网络上能提供的资料也只有支付宝官网的文档和上面 的一个php实现的博客。解决完让我豁然开朗,也希望还在炮坑的同学能从中受益。
目前我是在alipay ( 我觉得做的还可以 )基础上加入了证书签名。需要的朋友可以直接下载。
完整 Demo 的地址:https://github.com/00Kai0/pyAliPay/blob/master/demo.py