Fooling Go's X.509 Certificate Verification · Daniel Mangum
Below are two X.509 certificates. The<br>first is the Certificate Authority (CA) root certificate, and the second is a<br>leaf certifcate signed by the private key of the CA.
ca.crt.pem
-----BEGIN CERTIFICATE-----<br>MIIBejCCASGgAwIBAgIUda4UvlFzwQEO/fD0f4hAnj+ydPYwCgYIKoZIzj0EAwIw<br>EjEQMA4GA1UEAxMHUm9vdCBDQTAgFw0yNjAyMjcxOTQ3NDZaGA8yMTI2MDIwMzE5<br>NDc0NlowEjEQMA4GA1UEAxMHUm9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEH<br>A0IABKL5BB9aaQ2TtNgUymEsa/+s2ZlTXVll0N22KKWxh0N/JdgHcjrKfzqRlVrt<br>UN2GXdvsdLOq15TxBq97WvE07lKjUzBRMB0GA1UdDgQWBBTAVEw9doSzY1DuPVxP<br>EnwEp/+VJDAfBgNVHSMEGDAWgBTAVEw9doSzY1DuPVxPEnwEp/+VJDAPBgNVHRMB<br>Af8EBTADAQH/MAoGCCqGSM49BAMCA0cAMEQCIHrSTk/KJHAjn3MC/egvfxMM1NpG<br>GEzMB7EH+VXWz7RfAiAyhwy4E9hc8/qsTI+4iKf2o/zMRu5H2GNJOLqOngglbQ==<br>-----END CERTIFICATE-----
leaf.crt.pem
-----BEGIN CERTIFICATE-----<br>MIIBHjCBxAIULE3hvnYxU91g9c9H3+uGCSqXi4MwCgYIKoZIzj0EAwIwEjEQMA4G<br>A1UEAwwHUm9vdCBDQTAgFw0yNjAyMjcxOTQ3NDZaGA8yMTI2MDIwMzE5NDc0Nlow<br>DzENMAsGA1UEAwwEbGVhZjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKDZ21Yh<br>+1AQp1TrxrS8FquIVEHrFRSXncX9xl5vVhZFqvblzTp2Tg7TER5x7rHG1TIqQL1z<br>xDX4TB+nZOWkyAcwCgYIKoZIzj0EAwIDSQAwRgIhAMeo5t2d1RWL/SB0E+mvvIZP<br>jFT0wDWX1Bm26MtxRcf9AiEApG96fs70WF1JliFgzkTiNvbG7Gj4SvErZ9nNX/Lr<br>PnA=<br>-----END CERTIFICATE-----
If you downloaded these certificates, you could visually see that the latter<br>references the former as its Issuer. If you were to use a tool like openssl to<br>verify that the leaf is signed by the private key of root, you would see that it<br>is.
Unless of course you are reading this blog post from the year 2126 or you<br>have changed the system time on your machine. If the former, I am exceedingly<br>dissapointed that humanity is still using openssl.
openssl verify -CAfile ca.crt.pem leaf.crt.pem
Now, if you wanted to write a Go program that verified this chain of trust, it<br>might look something like the following.
main.go
package main
import (<br>"encoding/pem"<br>"crypto/x509"<br>"fmt"<br>"os"<br>"time"
func main() {<br>b, err := os.ReadFile("ca.crt.pem")<br>if err != nil {<br>panic(err)<br>block, _ := pem.Decode(b)<br>ca, err := x509.ParseCertificate(block.Bytes)<br>if err != nil {<br>panic(err)<br>b, err = os.ReadFile("leaf.crt.pem")<br>if err != nil {<br>panic(err)<br>block, _ = pem.Decode(b)<br>lc, err := x509.ParseCertificate(block.Bytes)<br>if err != nil {<br>panic(err)<br>roots := x509.NewCertPool()<br>roots.AddCert(ca)<br>opts := x509.VerifyOptions{<br>Roots: roots,<br>CurrentTime: time.Now(),<br>if _, err := lc.Verify(opts); err != nil {<br>panic(err)<br>fmt.Println("Certificate verification successful.")
But if you ran that program, you might be surprised to see the following.
panic: x509: certificate signed by unknown authority
If you used this CA certificate instead, you would see the expected output.
ca.verifies.crt.pem
-----BEGIN CERTIFICATE-----<br>MIIBejCCASGgAwIBAgIUda4UvlFzwQEO/fD0f4hAnj+ydPYwCgYIKoZIzj0EAwIw<br>EjEQMA4GA1UEAwwHUm9vdCBDQTAgFw0yNjAyMjcxOTQ3NDZaGA8yMTI2MDIwMzE5<br>NDc0NlowEjEQMA4GA1UEAwwHUm9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEH<br>A0IABKL5BB9aaQ2TtNgUymEsa/+s2ZlTXVll0N22KKWxh0N/JdgHcjrKfzqRlVrt<br>UN2GXdvsdLOq15TxBq97WvE07lKjUzBRMB0GA1UdDgQWBBTAVEw9doSzY1DuPVxP<br>EnwEp/+VJDAfBgNVHSMEGDAWgBTAVEw9doSzY1DuPVxPEnwEp/+VJDAPBgNVHRMB<br>Af8EBTADAQH/MAoGCCqGSM49BAMCA0cAMEQCIHrSTk/KJHAjn3MC/egvfxMM1NpG<br>GEzMB7EH+VXWz7RfAiAyhwy4E9hc8/qsTI+4iKf2o/zMRu5H2GNJOLqOngglbQ==<br>-----END CERTIFICATE-----
Certificate verification successful.
At first glance these certificates appear to be identical. You could use<br>openssl to view the contents of both certificates, and you would get identical<br>output.
openssl x509 -in ca.crt.pem -noout -text
Certificate:<br>Data:<br>Version: 3 (0x2)<br>Serial Number:<br>75:ae:14:be:51:73:c1:01:0e:fd:f0:f4:7f:88:40:9e:3f:b2:74:f6<br>Signature Algorithm: ecdsa-with-SHA256<br>Issuer: CN = Root CA<br>Validity<br>Not Before: Feb 27 19:47:46 2026 GMT<br>Not After : Feb 3 19:47:46 2126 GMT<br>Subject: CN = Root CA<br>Subject Public Key Info:<br>Public Key Algorithm: id-ecPublicKey<br>Public-Key: (256 bit)<br>pub:<br>04:a2:f9:04:1f:5a:69:0d:93:b4:d8:14:ca:61:2c:<br>6b:ff:ac:d9:99:53:5d:59:65:d0:dd:b6:28:a5:b1:<br>87:43:7f:25:d8:07:72:3a:ca:7f:3a:91:95:5a:ed:<br>50:dd:86:5d:db:ec:74:b3:aa:d7:94:f1:06:af:7b:<br>5a:f1:34:ee:52<br>ASN1 OID: prime256v1<br>NIST CURVE: P-256<br>X509v3 extensions:<br>X509v3 Subject Key Identifier:<br>C0:54:4C:3D:76:84:B3:63:50:EE:3D:5C:4F:12:7C:04:A7:FF:95:24<br>X509v3 Authority Key Identifier:<br>C0:54:4C:3D:76:84:B3:63:50:EE:3D:5C:4F:12:7C:04:A7:FF:95:24<br>X509v3 Basic Constraints: critical<br>CA:TRUE<br>Signature Algorithm: ecdsa-with-SHA256<br>Signature Value:<br>30:44:02:20:7a:d2:4e:4f:ca:24:70:23:9f:73:02:fd:e8:2f:<br>7f:13:0c:d4:da:46:18:4c:cc:07:b1:07:f9:55:d6:cf:b4:5f:<br>02:20:32:87:0c:b8:13:d8:5c:f3:fa:ac:4c:8f:b8:88:a7:f6:<br>a3:fc:cc:46:ee:47:d8:63:49:38:ba:8e:9e:08:25:6d
However, if you were to compare the bytes of the certificates, you would see<br>that there is a very slight difference; two bytes to be exact.
diff<br>4c4<br>> 00000030: 1231 1030 0e06 0355 0403 0c07 526f 6f74 .1.0...U....Root<br>8c8<br>> 00000070: 0c07 526f 6f74 2043 4130 5930 1306 072a ..Root CA0Y0...*
In both...