1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package main
17
18 import (
19 "bytes"
20 "crypto/tls"
21 "flag"
22 "fmt"
23 "net/url"
24 "os"
25 "strings"
26
27 "github.com/google/certificate-transparency-go/x509"
28 "github.com/google/certificate-transparency-go/x509util"
29 "k8s.io/klog/v2"
30 )
31
32 var (
33 root = flag.String("root", "", "Root CA certificate file")
34 intermediate = flag.String("intermediate", "", "Intermediate CA certificate file")
35 useSystemRoots = flag.Bool("system_roots", false, "Use system roots")
36 verbose = flag.Bool("verbose", false, "Verbose output")
37 strict = flag.Bool("strict", true, "Set non-zero exit code for non-fatal errors in parsing")
38 validate = flag.Bool("validate", false, "Validate certificate signatures")
39 checkTime = flag.Bool("check_time", false, "Check current validity of certificate")
40 checkName = flag.Bool("check_name", true, "Check certificate name validity")
41 checkEKU = flag.Bool("check_eku", true, "Check EKU nesting validity")
42 checkPathLen = flag.Bool("check_path_len", true, "Check path len constraint validity")
43 checkNameConstraint = flag.Bool("check_name_constraint", true, "Check name constraints")
44 checkUnknownCriticalExts = flag.Bool("check_unknown_critical_exts", true, "Check for unknown critical extensions")
45 checkRevoked = flag.Bool("check_revocation", false, "Check revocation status of certificate")
46 )
47
48 func addCerts(filename string, pool *x509.CertPool) {
49 if filename != "" {
50 dataList, err := x509util.ReadPossiblePEMFile(filename, "CERTIFICATE")
51 if err != nil {
52 klog.Exitf("Failed to read certificate file: %v", err)
53 }
54 for _, data := range dataList {
55 certs, err := x509.ParseCertificates(data)
56 if err != nil {
57 klog.Exitf("Failed to parse certificate from %s: %v", filename, err)
58 }
59 for _, cert := range certs {
60 pool.AddCert(cert)
61 }
62 }
63 }
64 }
65
66 func main() {
67 klog.InitFlags(nil)
68 flag.Parse()
69
70 failed := false
71 for _, target := range flag.Args() {
72 var err error
73 var chain []*x509.Certificate
74 if strings.HasPrefix(target, "https://") {
75 chain, err = chainFromSite(target)
76 } else {
77 chain, err = chainFromFile(target)
78 }
79 if err != nil {
80 klog.Errorf("%v", err)
81 }
82 if x509.IsFatal(err) {
83 failed = true
84 continue
85 } else if err != nil && *strict {
86 failed = true
87 }
88 for _, cert := range chain {
89 if *verbose {
90 fmt.Print(x509util.CertificateToString(cert))
91 }
92 if *checkRevoked {
93 if err := checkRevocation(cert, *verbose); err != nil {
94 klog.Errorf("%s: certificate is revoked: %v", target, err)
95 failed = true
96 }
97 }
98 }
99 if *validate && len(chain) > 0 {
100 opts := x509.VerifyOptions{
101 DisableTimeChecks: !*checkTime,
102 DisableCriticalExtensionChecks: !*checkUnknownCriticalExts,
103 DisableNameChecks: !*checkName,
104 DisableEKUChecks: !*checkEKU,
105 DisablePathLenChecks: !*checkPathLen,
106 DisableNameConstraintChecks: !*checkNameConstraint,
107 }
108 if err := validateChain(chain, opts, *root, *intermediate, *useSystemRoots); err != nil {
109 klog.Errorf("%s: verification error: %v", target, err)
110 failed = true
111 }
112 }
113 }
114 if failed {
115 os.Exit(1)
116 }
117 }
118
119
120
121
122 func chainFromSite(target string) ([]*x509.Certificate, error) {
123 u, err := url.Parse(target)
124 if err != nil {
125 return nil, fmt.Errorf("%s: failed to parse URL: %v", target, err)
126 }
127 if u.Scheme != "https" {
128 return nil, fmt.Errorf("%s: non-https URL provided", target)
129 }
130 host := u.Host
131 if !strings.Contains(host, ":") {
132 host += ":443"
133 }
134
135
136 conn, err := tls.Dial("tcp", host, &tls.Config{InsecureSkipVerify: true})
137 if err != nil {
138 return nil, fmt.Errorf("%s: failed to dial %q: %v", target, host, err)
139 }
140 defer conn.Close()
141
142
143 goChain := conn.ConnectionState().PeerCertificates
144 var nfe *x509.NonFatalErrors
145 chain := make([]*x509.Certificate, len(goChain))
146 for i, goCert := range goChain {
147 cert, err := x509.ParseCertificate(goCert.Raw)
148 if x509.IsFatal(err) {
149 return nil, fmt.Errorf("%s: failed to convert Go Certificate [%d]: %v", target, i, err)
150 } else if errs, ok := err.(x509.NonFatalErrors); ok {
151 nfe = nfe.Append(&errs)
152 } else if err != nil {
153 return nil, fmt.Errorf("%s: failed to convert Go Certificate [%d]: %v", target, i, err)
154 }
155 chain[i] = cert
156 }
157
158 if nfe.HasError() {
159 return chain, *nfe
160 }
161 return chain, nil
162 }
163
164
165
166
167 func chainFromFile(filename string) ([]*x509.Certificate, error) {
168 dataList, err := x509util.ReadPossiblePEMFile(filename, "CERTIFICATE")
169 if err != nil {
170 return nil, fmt.Errorf("%s: failed to read data: %v", filename, err)
171 }
172 var nfe *x509.NonFatalErrors
173 var chain []*x509.Certificate
174 for _, data := range dataList {
175 certs, err := x509.ParseCertificates(data)
176 if x509.IsFatal(err) {
177 return nil, fmt.Errorf("%s: failed to parse: %v", filename, err)
178 } else if errs, ok := err.(x509.NonFatalErrors); ok {
179 nfe = nfe.Append(&errs)
180 } else if err != nil {
181 return nil, fmt.Errorf("%s: failed to parse: %v", filename, err)
182 }
183 chain = append(chain, certs...)
184 }
185 if nfe.HasError() {
186 return chain, *nfe
187 }
188 return chain, nil
189 }
190
191 func validateChain(chain []*x509.Certificate, opts x509.VerifyOptions, rootsFile, intermediatesFile string, useSystemRoots bool) error {
192 roots := x509.NewCertPool()
193 if useSystemRoots {
194 systemRoots, err := x509.SystemCertPool()
195 if err != nil {
196 klog.Errorf("Failed to get system roots: %v", err)
197 }
198 roots = systemRoots
199 }
200 opts.KeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageAny}
201 opts.Roots = roots
202 opts.Intermediates = x509.NewCertPool()
203 addCerts(rootsFile, opts.Roots)
204 addCerts(intermediatesFile, opts.Intermediates)
205
206 if !useSystemRoots && len(rootsFile) == 0 {
207
208 if len(chain) > 1 {
209 last := chain[len(chain)-1]
210 if bytes.Equal(last.RawSubject, last.RawIssuer) {
211 opts.Roots.AddCert(last)
212 }
213 }
214 }
215 if len(intermediatesFile) == 0 {
216
217 for i := 1; i < len(chain); i++ {
218 opts.Intermediates.AddCert(chain[i])
219 }
220 }
221 _, err := chain[0].Verify(opts)
222 return err
223 }
224
225 func checkRevocation(cert *x509.Certificate, verbose bool) error {
226 for _, crldp := range cert.CRLDistributionPoints {
227 crlDataList, err := x509util.ReadPossiblePEMURL(crldp, "X509 CRL")
228 if err != nil {
229 klog.Errorf("failed to retrieve CRL from %q: %v", crldp, err)
230 continue
231 }
232 for _, crlData := range crlDataList {
233 crl, err := x509.ParseCertificateList(crlData)
234 if x509.IsFatal(err) {
235 klog.Errorf("failed to parse CRL from %q: %v", crldp, err)
236 continue
237 }
238 if err != nil {
239 klog.Errorf("non-fatal error parsing CRL from %q: %v", crldp, err)
240 }
241 if verbose {
242 fmt.Printf("\nRevocation data from %s:\n", crldp)
243 fmt.Print(x509util.CRLToString(crl))
244 }
245 for _, c := range crl.TBSCertList.RevokedCertificates {
246 if c.SerialNumber.Cmp(cert.SerialNumber) == 0 {
247 return fmt.Errorf("certificate is revoked since %v", c.RevocationTime)
248 }
249 }
250 }
251 }
252 return nil
253 }
254
View as plain text