golang官方包crypto/x509包对证书的容错性不好

最近工作中涉及特别多的安全相关的工作,其中一点需要解析某个第三方的x509的证书。在这个过程中竟然发现官方包crypto/x509的坑,所以特别记录在此。

废话不说,先上代码:

package main

import (
	"crypto/x509"
	"encoding/pem"
	"fmt"
)

func main() {
	certPEM := `-----BEGIN CERTIFICATE-----
MIID7jCCAtigAwIBAgIBATALBgkqhkiG9w0BAQswHTEbMBkGA1UEAxMSSHVhd2Vp
IEtleVN0b3JlICAgMB4XDTE5MDcyNDA4NTQ0NFoXDTI5MDcyNDA4NTQ0NFowGjEY
MBYGA1UEAxMPQSBLZXltYXN0ZXIgS2V5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEA8WS+DnC9JzGytDPpe3/GVY4xQx0bsPVP1Drf0n3eD+wq6U91QnjX
vhyRXtguDBu8OM5Qc5h8wFIOAUTD4+U/QGLQ3pZN+DXVmwlSJjbx8yMjuiUwInhG
uJ+xzhuEsNFCdxdyaNPmGhUycu09olL2/mcgDQFxusfr5jnnhU9VFv3/x1Y+7mVh
kICFnCE3YQ4ufHOHroQIE0kxnfJF+DkgK1tkdIMHEvX5rJvaqtd0M7RY/PRrE0dE
bYSlAlSAVqdjfLb0LF8tRTcjnIh4lg5NhqCWeAkGw8egUaNW1vFX6SeXrW6uJv2N
OeHxi9VFG7ymaOqB4URS0wGQ6dzoYsx/iwIDAQABo4IBPjCCATowCwYDVR0PBAQD
AgAAMAgGA1UdHwQBADCB9QYKKwYBBAHWeQIBEQSB5jCB4wIBAgoBAQIBAwoBAQR3
eyJjcHVfaWQiOiJIVUFXRUlfSFdITUFfNmYwNzk3ZTQtMWFjNS00YjM3LWEyZTgt
YzY2ZjQ2MTM0YjhlLTIyMzE5ZjcxIiwiY291bnRlciI6MTMsInVpZCI6IjEwNDE3
IiwicnNhX3Bzc19zYWx0bGVuIjozMn0EADASv4N3AgUAv4U9CAIGAWwjMJ1KMEah
CTEHAgUA/wEAAaIDAgEBowQCAggApQUxAwIBBKYFMQMCAQO/gUgFAgMBAAG/hT4D
AgEAv4VBBQIDAV+Qv4VCBQIDAxSxMCkGCSsGAQQBj1seAgEBAAQZMBcCAQCiAwEB
Ab+BSAswCaEHAwUABoAAgDALBgkqhkiG9w0BAQsDggEBAFZRjVpDqujJrwaZqycw
VgrM/J1b2VcVKUPJ39eJXs2S/ur7yUlgSxRcpOufa3IF0XekOUyHTNIroWUz/xLb
X6pv32PCMeavI/6ldl/zEJyy11PKX8ZrVfE05WiWUIJ6BwmDX+RtNjJSJ/xmwfDu
dn0CAx5apWsCMYpcGXQ2g8DRGQpYVdJS/aOTlDHGdSdSesx0TbGL39gjfdDb851L
spVFtcdoxw5nb0obwRItPBF+gHIh3xsYGGDN/EKSNN9YMja4MzgjTeLjNXjs1pXO
f4Fm3OiOfSFnTJuJk/rKQ0TiW+p3EvuZ+tRT+iffJvhdvDAIp7I3pJjaoZw4xwHH
Tr8=
-----END CERTIFICATE-----`
	block, _ := pem.Decode([]byte(certPEM))
	if block == nil {
		panic("failed to parse certificate PEM")
	}
	cert, err := x509.ParseCertificate(block.Bytes)
	if err != nil {
		panic("failed to parse certificate: " + err.Error())
	}
	fmt.Println(cert)
}

上面的代码执行到x509.ParseCertificate会失败,输出:

panic: failed to parse certificate: asn1: syntax error: truncated tag or length

因为第三方是一个大厂,所以按理说应该没有问题,所以使用java代码验证一下:

package com.mytest;


import java.io.ByteArrayInputStream;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;


public class Test {
    public static void main(String[] args) {

        try {


            String pem = "-----BEGIN CERTIFICATE-----\n" +
                    "MIID7jCCAtigAwIBAgIBATALBgkqhkiG9w0BAQswHTEbMBkGA1UEAxMSSHVhd2Vp\n" +
                    "IEtleVN0b3JlICAgMB4XDTE5MDcyNDA4NTQ0NFoXDTI5MDcyNDA4NTQ0NFowGjEY\n" +
                    "MBYGA1UEAxMPQSBLZXltYXN0ZXIgS2V5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\n" +
                    "MIIBCgKCAQEA8WS+DnC9JzGytDPpe3/GVY4xQx0bsPVP1Drf0n3eD+wq6U91QnjX\n" +
                    "vhyRXtguDBu8OM5Qc5h8wFIOAUTD4+U/QGLQ3pZN+DXVmwlSJjbx8yMjuiUwInhG\n" +
                    "uJ+xzhuEsNFCdxdyaNPmGhUycu09olL2/mcgDQFxusfr5jnnhU9VFv3/x1Y+7mVh\n" +
                    "kICFnCE3YQ4ufHOHroQIE0kxnfJF+DkgK1tkdIMHEvX5rJvaqtd0M7RY/PRrE0dE\n" +
                    "bYSlAlSAVqdjfLb0LF8tRTcjnIh4lg5NhqCWeAkGw8egUaNW1vFX6SeXrW6uJv2N\n" +
                    "OeHxi9VFG7ymaOqB4URS0wGQ6dzoYsx/iwIDAQABo4IBPjCCATowCwYDVR0PBAQD\n" +
                    "AgAAMAgGA1UdHwQBADCB9QYKKwYBBAHWeQIBEQSB5jCB4wIBAgoBAQIBAwoBAQR3\n" +
                    "eyJjcHVfaWQiOiJIVUFXRUlfSFdITUFfNmYwNzk3ZTQtMWFjNS00YjM3LWEyZTgt\n" +
                    "YzY2ZjQ2MTM0YjhlLTIyMzE5ZjcxIiwiY291bnRlciI6MTMsInVpZCI6IjEwNDE3\n" +
                    "IiwicnNhX3Bzc19zYWx0bGVuIjozMn0EADASv4N3AgUAv4U9CAIGAWwjMJ1KMEah\n" +
                    "CTEHAgUA/wEAAaIDAgEBowQCAggApQUxAwIBBKYFMQMCAQO/gUgFAgMBAAG/hT4D\n" +
                    "AgEAv4VBBQIDAV+Qv4VCBQIDAxSxMCkGCSsGAQQBj1seAgEBAAQZMBcCAQCiAwEB\n" +
                    "Ab+BSAswCaEHAwUABoAAgDALBgkqhkiG9w0BAQsDggEBAFZRjVpDqujJrwaZqycw\n" +
                    "VgrM/J1b2VcVKUPJ39eJXs2S/ur7yUlgSxRcpOufa3IF0XekOUyHTNIroWUz/xLb\n" +
                    "X6pv32PCMeavI/6ldl/zEJyy11PKX8ZrVfE05WiWUIJ6BwmDX+RtNjJSJ/xmwfDu\n" +
                    "dn0CAx5apWsCMYpcGXQ2g8DRGQpYVdJS/aOTlDHGdSdSesx0TbGL39gjfdDb851L\n" +
                    "spVFtcdoxw5nb0obwRItPBF+gHIh3xsYGGDN/EKSNN9YMja4MzgjTeLjNXjs1pXO\n" +
                    "f4Fm3OiOfSFnTJuJk/rKQ0TiW+p3EvuZ+tRT+iffJvhdvDAIp7I3pJjaoZw4xwHH\n" +
                    "Tr8=\n" +
                    "-----END CERTIFICATE-----";

            CertificateFactory factory = CertificateFactory.getInstance("X.509");
            X509Certificate askCertificate = (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(pem.getBytes()));

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

意外发生了,java没有异常!

这样问题就来了,难道golang实现和java实现不一致? 然后找一个更权威的工具openssl来验证一下:

openssl x509 -pubkey -noout -in cert.pem

输出也没有问题!

因为自己对x509真心不太懂,就提了一个问题,本来基本就没有报希望的,没想到,竟然有真心懂安全和golang的外国友人帮忙check了一下证书细节。下面是他的回复:

I think the extensions in your certificate might be invalid. Namely CRL Distribution Points: 2.5.29.31

X509v3 CRL Distribution Points: 
SEQUENCE {
 437    3:           OBJECT IDENTIFIER cRLDistributionPoints (2 5 29 31)
 442    1:           OCTET STRING 00 
         :           }                .

Per the RFC https://www.ietf.org/rfc/rfc5280.txt, the cRLDistributionPoints has to respect a certain definition and is sequence of distributionPoint(s). In your case it seems to be empty.
I could parse this certificate with Python's x509 library and openssl command as well, but I think these implementations might be lenient in parsing non-critical extensions.

An example of valid cRLDistributionPoints extension would be:

X509v3 CRL Distribution Points: 
 
                Full Name:
                  URI:ldap://www.example.com/ldap?DN=TEST
 
                Full Name:
                  URI:http://www.example.com/crl/test.crl

大概意思就是说golang的x509实现是按照rfc5280来的,但是这个第三方证书不是完全标准的格式,但是java、python和openssl都可以正确解析。golang的crypto/x509包对不满足标准的可能容错性不足。

太感谢这位网友了,解决了我的疑惑!

本着负责的态度,我给golang官方提了一个issue。哈哈,第一次给官方提了issue,感觉离柴大又近了那么一毫米。。。:)

虽然此次使用golang官方包采坑了,但是依然没有减少我对golang的热情,人都没有完美的,更何况人做出来的东西了?希望golang越来越好!

PS:如果有朋友也要用golang做安全相关的工作,可以尽量使用openssl这个命令行来做,一般情况下性能也够了吧,实在不行,可以考虑将openssl的lib作为cgo引入golang?这点只是一个思路,没有做过。

UPDATE 已经有人做了openssl lib在golang的binding, spacemonkeygo/openssl。可以考虑使用。