Skip to content
HN On Hacker News ↗

Fooling Go's X.509 Certificate Verification

▲ 28 points 15 comments by hasheddan 2d ago HN discussion ↗

Pangram verdict · v3.3

We believe that this document is fully human-written

2 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 11 of 11
SEGMENTS · AI 0 of 11
WORD COUNT 1,196
PEAK AI % 1% · §2
Analyzed
Jun 8
backend: pangram/v3.3
Segments scanned
11 windows
avg 109 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 1,196 words · 11 segments analyzed

Human AI-generated
§1 Human · 1%

Below are two X.509 certificates. The first is the Certificate Authority (CA) root certificate, and the second is a leaf certifcate signed by the private key of the CA. ca.crt.pem -----BEGIN CERTIFICATE----- MIIBejCCASGgAwIBAgIUda4UvlFzwQEO/fD0f4hAnj+ydPYwCgYIKoZIzj0EAwIw EjEQMA4GA1UEAxMHUm9vdCBDQTAgFw0yNjAyMjcxOTQ3NDZaGA8yMTI2MDIwMzE5 NDc0NlowEjEQMA4GA1UEAxMHUm9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEH A0IABKL5BB9aaQ2TtNgUymEsa/+s2ZlTXVll0N22KKWxh0N/JdgHcjrKfzqRlVrt UN2GXdvsdLOq15TxBq97WvE07lKjUzBRMB0GA1UdDgQWBBTAVEw9doSzY1DuPVxP EnwEp/+VJDAfBgNVHSMEGDAWgBTAVEw9doSzY1DuPVxPEnwEp/+VJDAPBgNVHRMB Af8EBTADAQH/MAoGCCqGSM49BAMCA0cAMEQCIHrSTk/KJHAjn3MC/egvfxMM1NpG GEzMB7EH+VXWz7RfAiAyhwy4E9hc8/qsTI+4iKf2o/zMRu5H2GNJOLqOngglbQ== -----END CERTIFICATE-----

§2 Human · 1%

leaf.crt.pem -----BEGIN CERTIFICATE----- MIIBHjCBxAIULE3hvnYxU91g9c9H3+uGCSqXi4MwCgYIKoZIzj0EAwIwEjEQMA4G A1UEAwwHUm9vdCBDQTAgFw0yNjAyMjcxOTQ3NDZaGA8yMTI2MDIwMzE5NDc0Nlow DzENMAsGA1UEAwwEbGVhZjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKDZ21Yh +1AQp1TrxrS8FquIVEHrFRSXncX9xl5vVhZFqvblzTp2Tg7TER5x7rHG1TIqQL1z xDX4TB+nZOWkyAcwCgYIKoZIzj0EAwIDSQAwRgIhAMeo5t2d1RWL/SB0E+mvvIZP jFT0wDWX1Bm26MtxRcf9AiEApG96fs70WF1JliFgzkTiNvbG7Gj4SvErZ9nNX/Lr PnA= -----END CERTIFICATE----- If you downloaded these certificates, you could visually see that the latter references the former as its Issuer. If you were to use a tool like openssl to verify that the leaf is signed by the private key of root, you would see that it is.

Unless of course you are reading this blog post from the year 2126 or you have changed the system time on your machine. If the former, I am exceedingly 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 might look something like the following.

§3 Human · 1%

main.go package main

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

func main() { b, err := os.ReadFile("ca.crt.pem") if err != nil { panic(err) } block, _ := pem.Decode(b) ca, err := x509.ParseCertificate(block.Bytes) if err != nil { panic(err) } b, err = os.ReadFile("leaf.crt.pem") if err != nil { panic(err) } block, _ = pem.Decode(b) lc, err := x509.ParseCertificate(block.Bytes) if err != nil { panic(err) } roots := x509.NewCertPool() roots.AddCert(ca) opts := x509.VerifyOptions{ Roots: roots, CurrentTime: time.Now(), } if _, err := lc.Verify(opts); err != nil { panic(err) } 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----- MIIBejCCASGgAwIBAgIUda4UvlFzwQEO/fD0f4hAnj+ydPYwCgYIKoZIzj0EAwIw EjEQMA4GA1UEAwwHUm9vdCBDQTAgFw0yNjAyMjcxOTQ3NDZaGA8yMTI2MDIwMzE5 NDc0NlowEjEQMA4GA1UEAwwHUm9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEH A0IABKL5BB9aaQ2TtNgUymEsa/+s2ZlTXVll0N22KKWxh0N/JdgHcjrKfzqRlVrt

§4 Human · 1%

UN2GXdvsdLOq15TxBq97WvE07lKjUzBRMB0GA1UdDgQWBBTAVEw9doSzY1DuPVxP EnwEp/+VJDAfBgNVHSMEGDAWgBTAVEw9doSzY1DuPVxPEnwEp/+VJDAPBgNVHRMB Af8EBTADAQH/MAoGCCqGSM49BAMCA0cAMEQCIHrSTk/KJHAjn3MC/egvfxMM1NpG GEzMB7EH+VXWz7RfAiAyhwy4E9hc8/qsTI+4iKf2o/zMRu5H2GNJOLqOngglbQ== -----END CERTIFICATE----- Certificate verification successful. At first glance these certificates appear to be identical. You could use openssl to view the contents of both certificates, and you would get identical output. openssl x509 -in ca.crt.pem -noout -text Certificate: Data: Version: 3 (0x2) Serial Number: 75:ae:14:be:51:73:c1:01:0e:fd:f0:f4:7f:88:40:9e:3f:b2:74:f6 Signature Algorithm: ecdsa-with-SHA256 Issuer: CN = Root CA Validity Not Before: Feb 27 19:47:46 2026 GMT Not After : Feb 3 19:47:46 2126 GMT Subject: CN = Root CA Subject Public Key Info: Public Key Algorithm: id-ecPublicKey Public-Key: (256 bit) pub: 04:a2:f9:04:1f:5a:69:0d:93:b4:d8:14:ca:61:2c:

§5 Human · 1%

6b:ff:ac:d9:99:53:5d:59:65:d0:dd:b6:28:a5:b1: 87:43:7f:25:d8:07:72:3a:ca:7f:3a:91:95:5a:ed: 50:dd:86:5d:db:ec:74:b3:aa:d7:94:f1:06:af:7b: 5a:f1:34:ee:52 ASN1 OID: prime256v1 NIST CURVE: P-256 X509v3 extensions: X509v3 Subject Key Identifier: C0:54:4C:3D:76:84:B3:63:50:EE:3D:5C:4F:12:7C:04:A7:FF:95:24 X509v3 Authority Key Identifier: C0:54:4C:3D:76:84:B3:63:50:EE:3D:5C:4F:12:7C:04:A7:FF:95:24 X509v3 Basic Constraints: critical CA:TRUE Signature Algorithm: ecdsa-with-SHA256 Signature Value: 30:44:02:20:7a:d2:4e:4f:ca:24:70:23:9f:73:02:fd:e8:2f:

§6 Human · 0%

7f:13:0c:d4:da:46:18:4c:cc:07:b1:07:f9:55:d6:cf:b4:5f: 02:20:32:87:0c:b8:13:d8:5c:f3:fa:ac:4c:8f:b8:88:a7:f6: 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 that there is a very slight difference; two bytes to be exact. diff <(openssl x509 -in ca.crt.pem -outform der | xxd) <(openssl x509 -in ca.verifies.crt.pem -outform der | xxd) 4c4 < 00000030: 1231 1030 0e06 0355 0403 1307 526f 6f74 .1.0...U....Root --- > 00000030: 1231 1030 0e06 0355 0403 0c07 526f 6f74 .1.0...U....Root 8c8 < 00000070: 1307 526f 6f74 2043

§7 Human · 0%

4130 5930 1306 072a ..Root CA0Y0...* --- > 00000070: 0c07 526f 6f74 2043 4130 5930 1306 072a ..Root CA0Y0...* In both locations, there is a 0x13 byte in the certificate which failed verification with the Go program (ca.crt.pem), and a 0x0c byte in the certificate that passed (ca.verifies.crt.pem). If you are familiar with X.509 certificates, you’ll know that they are defined using Abstract Syntax Notation One (ASN.1) and are binary encoded using Distinguished Encoding Rules (DER). They are frequently Base64 encoded and stored and transmitted as Privacy-enhanced Mail (PEM) text files (which you have already seen in this post). The ASN.1 specification defines a set of data types, each with an associated tag (non-negative integer identifier), which precedes the length and the value when using DER encoding (see this post from Let’s Encrypt for more information). openssl can once again be used to see the data types of different fields in the certificate that is successfully verified. openssl asn1parse -in ca.verifies.crt.pem 0:d=0 hl=4 l= 378 cons: SEQUENCE 4:d=1 hl=4 l= 289 cons: SEQUENCE 8:d=2 hl=2 l= 3 cons: cont [ 0 ] 10:d=3 hl=2 l= 1 prim: INTEGER :02 13:d=2 hl=2 l= 20 prim: INTEGER :75AE14BE5173C1010EFDF0F47F88409E3FB274F6

§8 Human · 1%

35:d=2 hl=2 l= 10 cons: SEQUENCE 37:d=3 hl=2 l= 8 prim: OBJECT :ecdsa-with-SHA256 47:d=2 hl=2 l= 18 cons: SEQUENCE 49:d=3 hl=2 l= 16 cons: SET 51:d=4 hl=2 l= 14 cons: SEQUENCE 53:d=5 hl=2 l= 3 prim: OBJECT :commonName 58:d=5 hl=2 l= 7 prim: UTF8STRING :Root CA 67:d=2 hl=2 l= 32 cons: SEQUENCE 69:d=3 hl=2 l= 13 prim: UTCTIME :260227194746Z 84:d=3 hl=2 l= 15 prim: GENERALIZEDTIME :21260203194746Z 101:d=2 hl=2 l= 18 cons: SEQUENCE 103:d=3 hl=2 l= 16 cons: SET 105:d=4 hl=2 l= 14 cons: SEQUENCE 107:d=5 hl=2 l= 3 prim: OBJECT :commonName 112:d=5 hl=2 l= 7 prim: UTF8STRING :Root CA 121:d=2 hl=2 l= 89 cons: SEQUENCE 123:d=3 hl=2 l= 19 cons: SEQUENCE 125:d=4 hl=2 l= 7 prim: OBJECT :id-ecPublicKey

§9 Human · 1%

134:d=4 hl=2 l= 8 prim: OBJECT :prime256v1 144:d=3 hl=2 l= 66 prim: BIT STRING 212:d=2 hl=2 l= 83 cons: cont [ 3 ] 214:d=3 hl=2 l= 81 cons: SEQUENCE 216:d=4 hl=2 l= 29 cons: SEQUENCE 218:d=5 hl=2 l= 3 prim: OBJECT :X509v3 Subject Key Identifier 223:d=5 hl=2 l= 22 prim: OCTET STRING [HEX DUMP]:0414C0544C3D7684B36350EE3D5C4F127C04A7FF9524 247:d=4 hl=2 l= 31 cons: SEQUENCE 249:d=5 hl=2 l= 3 prim: OBJECT :X509v3 Authority Key Identifier 254:d=5 hl=2 l= 24 prim: OCTET STRING [HEX DUMP]:30168014C0544C3D7684B36350EE3D5C4F127C04A7FF9524 280:d=4 hl=2 l= 15 cons: SEQUENCE 282:d=5 hl=2 l= 3 prim: OBJECT :X509v3 Basic Constraints 287:d=5 hl=2 l= 1 prim:

§10 Human · 1%

BOOLEAN :255 290:d=5 hl=2 l= 5 prim: OCTET STRING [HEX DUMP]:30030101FF 297:d=1 hl=2 l= 10 cons: SEQUENCE 299:d=2 hl=2 l= 8 prim: OBJECT :ecdsa-with-SHA256 309:d=1 hl=2 l= 71 prim: BIT STRING In the diff view of the two CA certificates, the bytes that differed preceded the Root CA string in two different places: the Subject and the Issuer, which are the same since this is a self-signed certificate. They were also followed by a 0x07 byte, which aligns with the number of characters in Root CA (i.e. the length of the value). The differing leading byte suggests differing ASN.1 data types for these fields. The CA certificate for which validation is successful uses UTF8String (0x0c), and you can use openssl with the failing CA certificate to see that it uses PrintableString instead (0x13). openssl asn1parse -in ca.crt.pem 0:d=0 hl=4 l= 378 cons: SEQUENCE 4:d=1 hl=4 l= 289 cons: SEQUENCE 8:d=2 hl=2 l= 3 cons: cont [ 0 ] 10:d=3 hl=2 l= 1 prim: INTEGER :02 13:d=2 hl=2 l= 20 prim: INTEGER :75AE14BE5173C1010EFDF0F47F88409E3FB274F6

§11 Human · 1%

35:d=2 hl=2 l= 10 cons: SEQUENCE 37:d=3 hl=2 l= 8 prim: OBJECT :ecdsa-with-SHA256 47:d=2 hl=2 l= 18 cons: SEQUENCE 49:d=3 hl=2 l= 16 cons: SET 51:d=4 hl=2 l= 14 cons: SEQUENCE 53:d=5 hl=2 l= 3 prim: OBJECT :commonName 58:d=5 hl=2 l= 7 prim: PRINTABLESTRING :Root CA 67:d=2 hl=2 l= 32 cons: SEQUENCE 69:d=3 hl=2 l= 13 prim: UTCTIME :260227194746Z 84:d=3 hl=2 l= 15 prim: GENERALIZEDTIME :21260203194746Z 101:d=2 hl=2 l= 18 cons: SEQUENCE 103:d=3 hl=2 l= 16 cons: SET 105:d=4 hl=2 l= 14 cons: SEQUENCE 107:d=5 hl=2 l= 3 prim: OBJECT :commonName 112:d=5 hl=2 l= 7 prim: PRINTABLESTRING :Root CA 121:d=2 hl=2 l= 89 cons: SEQUENCE 123:d=3 hl=2 l= 19 cons: SEQUENCE 125:d=4 hl=2