├── example_test.go ├── Makefile ├── client.go ├── README.md └── script └── certdata2pem.py /example_test.go: -------------------------------------------------------------------------------- 1 | package stdroots 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | ) 7 | 8 | func ExampleClient() { 9 | resp, err := Client.Get("https://www.google.com") 10 | if err != nil { 11 | log.Fatal(err) 12 | } 13 | fmt.Println(resp.Status) 14 | // Output: 200 OK 15 | } 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # The raw certs from mozilla 2 | CERTDATA:="https://hg.mozilla.org/releases/mozilla-release/raw-file/default/security/nss/lib/ckfw/builtins/certdata.txt" 3 | 4 | update: 5 | # Delete and recreate the directory 6 | rm -rf certs 7 | mkdir certs || exit 1 8 | 9 | # certdata.txt must be in current directory 10 | curl -o certs/certdata.txt $(CERTDATA) || exit 1 11 | 12 | # run python script to convert to crts 13 | cd certs && python2.7 ../script/certdata2pem.py || exit 1 14 | 15 | # remove files that should not end up in bindata 16 | rm certs/*.p11-kit certs/*.txt 17 | 18 | # generate new bindata file 19 | go generate 20 | 21 | 22 | test: 23 | go test -v 24 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | //go:generate go-bindata -o bindata.go -pkg stdroots certs/... 2 | 3 | package stdroots 4 | 5 | import ( 6 | "crypto/tls" 7 | "crypto/x509" 8 | "fmt" 9 | "log" 10 | "net/http" 11 | ) 12 | 13 | // Client is an HTTP client preloaded with the certificates bundled 14 | // with this package 15 | var Client *http.Client 16 | 17 | // Pool is an x509 certificate pool containing each of the certifications 18 | // bundled with this package. 19 | var Pool *x509.CertPool 20 | 21 | func init() { 22 | // get list of certs 23 | fs, err := AssetDir("certs") 24 | if err != nil { 25 | // assets are bundled with binary so it makes sense to panic here 26 | panic(fmt.Sprintf(`AssetDir("certs") failed: %v`, err)) 27 | } 28 | 29 | // load and parse certs 30 | Pool = x509.NewCertPool() 31 | for _, f := range fs { 32 | ok := Pool.AppendCertsFromPEM(MustAsset("certs/" + f)) 33 | if !ok { 34 | // assets are tested before release so it makes sense to panic here 35 | panic(fmt.Sprintf("could not load cert from %s: AppendCertsFromPEM returned false", f)) 36 | log.Println("failed to append cert:", f) 37 | } 38 | } 39 | 40 | // setup HTTP client 41 | tlsConfig := &tls.Config{ 42 | RootCAs: Pool, 43 | } 44 | tlsConfig.BuildNameToCertificate() 45 | transport := &http.Transport{TLSClientConfig: tlsConfig} 46 | 47 | Client = &http.Client{Transport: transport} 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Standard CA Roots for Golang 2 | 3 | This package provides an HTTP client preloaded with the [standard Mozilla CA roots](https://hg.mozilla.org/releases/mozilla-release/raw-file/default/security/nss/lib/ckfw/builtins/certdata.txt). This can be helpful for TLS clients in environments that do not provide a standard set of CA roots (e.g. extremely minimal Docker containers). 4 | 5 | ### Security warning 6 | 7 | If you ship code that uses package then you will be shipping CA roots that will not update except when you ship updates to your code. This means that when a root CA is revealed to be untrustworthy, such as in the recent Wosign incident, your code will continue trusting an untrustworthy CA until you update it. In contrast, if you get CA roots from the underlying operating system (which is the default in Golang) then this issue will be taken care of by updates from your operating system vendor. This package should therefore only be used in situations where it will be updated regularly, and only when you cannot get CA roots from the underlying operating system. 8 | 9 | ### Quick start 10 | 11 | First install the package: 12 | ```shell 13 | go get github.com/alexflint/stdroots 14 | ``` 15 | 16 | Then use `stdroots.Client`, which is a `*http.Client`: 17 | ```go 18 | resp, err := stdroots.Client.Get("https://www.google.com") 19 | ``` 20 | 21 | ### Reproducibility 22 | 23 | The certificates are embedded in the `bindata.go` file. You can reproduce this file by running `make update`, which will pull `certdata.txt` from the latest Mozilla release and generate a set of certificates under `certs/`, then finally generate `bindata.go`. 24 | -------------------------------------------------------------------------------- /script/certdata2pem.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # vim:set et sw=4: 3 | # 4 | # certdata2pem.py - splits certdata.txt into multiple files 5 | # 6 | # Copyright (C) 2009 Philipp Kern 7 | # Copyright (C) 2013 Kai Engert 8 | # 9 | # This program is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation; either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with this program; if not, write to the Free Software 21 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, 22 | # USA. 23 | 24 | import base64 25 | import os.path 26 | import re 27 | import sys 28 | import textwrap 29 | import urllib 30 | 31 | objects = [] 32 | 33 | def printable_serial(obj): 34 | return ".".join(map(lambda x:str(ord(x)), obj['CKA_SERIAL_NUMBER'])) 35 | 36 | # Dirty file parser. 37 | in_data, in_multiline, in_obj = False, False, False 38 | field, type, value, obj = None, None, None, dict() 39 | for line in open('certdata.txt', 'r'): 40 | # Ignore the file header. 41 | if not in_data: 42 | if line.startswith('BEGINDATA'): 43 | in_data = True 44 | continue 45 | # Ignore comment lines. 46 | if line.startswith('#'): 47 | continue 48 | # Empty lines are significant if we are inside an object. 49 | if in_obj and len(line.strip()) == 0: 50 | objects.append(obj) 51 | obj = dict() 52 | in_obj = False 53 | continue 54 | if len(line.strip()) == 0: 55 | continue 56 | if in_multiline: 57 | if not line.startswith('END'): 58 | if type == 'MULTILINE_OCTAL': 59 | line = line.strip() 60 | for i in re.finditer(r'\\([0-3][0-7][0-7])', line): 61 | value += chr(int(i.group(1), 8)) 62 | else: 63 | value += line 64 | continue 65 | obj[field] = value 66 | in_multiline = False 67 | continue 68 | if line.startswith('CKA_CLASS'): 69 | in_obj = True 70 | line_parts = line.strip().split(' ', 2) 71 | if len(line_parts) > 2: 72 | field, type = line_parts[0:2] 73 | value = ' '.join(line_parts[2:]) 74 | elif len(line_parts) == 2: 75 | field, type = line_parts 76 | value = None 77 | else: 78 | raise NotImplementedError, 'line_parts < 2 not supported.\n' + line 79 | if type == 'MULTILINE_OCTAL': 80 | in_multiline = True 81 | value = "" 82 | continue 83 | obj[field] = value 84 | if len(obj.items()) > 0: 85 | objects.append(obj) 86 | 87 | # Build up trust database. 88 | trustmap = dict() 89 | for obj in objects: 90 | if obj['CKA_CLASS'] != 'CKO_NSS_TRUST': 91 | continue 92 | key = obj['CKA_LABEL'] + printable_serial(obj) 93 | trustmap[key] = obj 94 | print " added trust", key 95 | 96 | # Build up cert database. 97 | certmap = dict() 98 | for obj in objects: 99 | if obj['CKA_CLASS'] != 'CKO_CERTIFICATE': 100 | continue 101 | key = obj['CKA_LABEL'] + printable_serial(obj) 102 | certmap[key] = obj 103 | print " added cert", key 104 | 105 | def obj_to_filename(obj): 106 | label = obj['CKA_LABEL'][1:-1] 107 | label = label.replace('/', '_')\ 108 | .replace(' ', '_')\ 109 | .replace('(', '=')\ 110 | .replace(')', '=')\ 111 | .replace(',', '_') 112 | label = re.sub(r'\\x[0-9a-fA-F]{2}', lambda m:chr(int(m.group(0)[2:], 16)), label) 113 | serial = printable_serial(obj) 114 | return label + ":" + serial 115 | 116 | trust_types = { 117 | "CKA_TRUST_DIGITAL_SIGNATURE": "digital-signature", 118 | "CKA_TRUST_NON_REPUDIATION": "non-repudiation", 119 | "CKA_TRUST_KEY_ENCIPHERMENT": "key-encipherment", 120 | "CKA_TRUST_DATA_ENCIPHERMENT": "data-encipherment", 121 | "CKA_TRUST_KEY_AGREEMENT": "key-agreement", 122 | "CKA_TRUST_KEY_CERT_SIGN": "cert-sign", 123 | "CKA_TRUST_CRL_SIGN": "crl-sign", 124 | "CKA_TRUST_SERVER_AUTH": "server-auth", 125 | "CKA_TRUST_CLIENT_AUTH": "client-auth", 126 | "CKA_TRUST_CODE_SIGNING": "code-signing", 127 | "CKA_TRUST_EMAIL_PROTECTION": "email-protection", 128 | "CKA_TRUST_IPSEC_END_SYSTEM": "ipsec-end-system", 129 | "CKA_TRUST_IPSEC_TUNNEL": "ipsec-tunnel", 130 | "CKA_TRUST_IPSEC_USER": "ipsec-user", 131 | "CKA_TRUST_TIME_STAMPING": "time-stamping", 132 | "CKA_TRUST_STEP_UP_APPROVED": "step-up-approved", 133 | } 134 | 135 | legacy_trust_types = { 136 | "LEGACY_CKA_TRUST_SERVER_AUTH": "server-auth", 137 | "LEGACY_CKA_TRUST_CODE_SIGNING": "code-signing", 138 | "LEGACY_CKA_TRUST_EMAIL_PROTECTION": "email-protection", 139 | } 140 | 141 | legacy_to_real_trust_types = { 142 | "LEGACY_CKA_TRUST_SERVER_AUTH": "CKA_TRUST_SERVER_AUTH", 143 | "LEGACY_CKA_TRUST_CODE_SIGNING": "CKA_TRUST_CODE_SIGNING", 144 | "LEGACY_CKA_TRUST_EMAIL_PROTECTION": "CKA_TRUST_EMAIL_PROTECTION", 145 | } 146 | 147 | openssl_trust = { 148 | "CKA_TRUST_SERVER_AUTH": "serverAuth", 149 | "CKA_TRUST_CLIENT_AUTH": "clientAuth", 150 | "CKA_TRUST_CODE_SIGNING": "codeSigning", 151 | "CKA_TRUST_EMAIL_PROTECTION": "emailProtection", 152 | } 153 | 154 | for tobj in objects: 155 | if tobj['CKA_CLASS'] == 'CKO_NSS_TRUST': 156 | key = tobj['CKA_LABEL'] + printable_serial(tobj) 157 | print "producing trust for " + key 158 | trustbits = [] 159 | distrustbits = [] 160 | openssl_trustflags = [] 161 | openssl_distrustflags = [] 162 | legacy_trustbits = [] 163 | legacy_openssl_trustflags = [] 164 | for t in trust_types.keys(): 165 | if tobj.has_key(t) and tobj[t] == 'CKT_NSS_TRUSTED_DELEGATOR': 166 | trustbits.append(t) 167 | if t in openssl_trust: 168 | openssl_trustflags.append(openssl_trust[t]) 169 | if tobj.has_key(t) and tobj[t] == 'CKT_NSS_NOT_TRUSTED': 170 | distrustbits.append(t) 171 | if t in openssl_trust: 172 | openssl_distrustflags.append(openssl_trust[t]) 173 | 174 | for t in legacy_trust_types.keys(): 175 | if tobj.has_key(t) and tobj[t] == 'CKT_NSS_TRUSTED_DELEGATOR': 176 | real_t = legacy_to_real_trust_types[t] 177 | legacy_trustbits.append(real_t) 178 | if real_t in openssl_trust: 179 | legacy_openssl_trustflags.append(openssl_trust[real_t]) 180 | if tobj.has_key(t) and tobj[t] == 'CKT_NSS_NOT_TRUSTED': 181 | raise NotImplementedError, 'legacy distrust not supported.\n' + line 182 | 183 | fname = obj_to_filename(tobj) 184 | try: 185 | obj = certmap[key] 186 | except: 187 | obj = None 188 | 189 | if obj != None: 190 | fname += ".crt" 191 | else: 192 | fname += ".p11-kit" 193 | 194 | is_legacy = 0 195 | if tobj.has_key('LEGACY_CKA_TRUST_SERVER_AUTH') or tobj.has_key('LEGACY_CKA_TRUST_EMAIL_PROTECTION') or tobj.has_key('LEGACY_CKA_TRUST_CODE_SIGNING'): 196 | is_legacy = 1 197 | if obj == None: 198 | raise NotImplementedError, 'found legacy trust without certificate.\n' + line 199 | legacy_fname = "legacy-default/" + fname 200 | f = open(legacy_fname, 'w') 201 | f.write("# alias=%s\n"%tobj['CKA_LABEL']) 202 | f.write("# trust=" + " ".join(legacy_trustbits) + "\n") 203 | if legacy_openssl_trustflags: 204 | f.write("# openssl-trust=" + " ".join(legacy_openssl_trustflags) + "\n") 205 | f.write("-----BEGIN CERTIFICATE-----\n") 206 | f.write("\n".join(textwrap.wrap(base64.b64encode(obj['CKA_VALUE']), 64))) 207 | f.write("\n-----END CERTIFICATE-----\n") 208 | f.close() 209 | if tobj.has_key('CKA_TRUST_SERVER_AUTH') or tobj.has_key('CKA_TRUST_EMAIL_PROTECTION') or tobj.has_key('CKA_TRUST_CODE_SIGNING'): 210 | fname = "legacy-disable/" + fname 211 | else: 212 | continue 213 | 214 | f = open(fname, 'w') 215 | if obj != None: 216 | f.write("# alias=%s\n"%tobj['CKA_LABEL']) 217 | f.write("# trust=" + " ".join(trustbits) + "\n") 218 | f.write("# distrust=" + " ".join(distrustbits) + "\n") 219 | if openssl_trustflags: 220 | f.write("# openssl-trust=" + " ".join(openssl_trustflags) + "\n") 221 | if openssl_distrustflags: 222 | f.write("# openssl-distrust=" + " ".join(openssl_distrustflags) + "\n") 223 | f.write("-----BEGIN CERTIFICATE-----\n") 224 | f.write("\n".join(textwrap.wrap(base64.b64encode(obj['CKA_VALUE']), 64))) 225 | f.write("\n-----END CERTIFICATE-----\n") 226 | else: 227 | f.write("[p11-kit-object-v1]\n") 228 | f.write("label: "); 229 | f.write(tobj['CKA_LABEL']); 230 | f.write("\n") 231 | f.write("class: certificate\n") 232 | f.write("certificate-type: x-509\n") 233 | f.write("issuer: \""); 234 | f.write(urllib.quote(tobj['CKA_ISSUER'])); 235 | f.write("\"\n") 236 | f.write("serial-number: \""); 237 | f.write(urllib.quote(tobj['CKA_SERIAL_NUMBER'])); 238 | f.write("\"\n") 239 | if (tobj['CKA_TRUST_SERVER_AUTH'] == 'CKT_NSS_NOT_TRUSTED') or (tobj['CKA_TRUST_EMAIL_PROTECTION'] == 'CKT_NSS_NOT_TRUSTED') or (tobj['CKA_TRUST_CODE_SIGNING'] == 'CKT_NSS_NOT_TRUSTED'): 240 | f.write("x-distrusted: true\n") 241 | f.write("\n\n") 242 | f.close() 243 | print " -> written as '%s', trust = %s, openssl-trust = %s, distrust = %s, openssl-distrust = %s" % (fname, trustbits, openssl_trustflags, distrustbits, openssl_distrustflags) 244 | --------------------------------------------------------------------------------