1// Copyright 2016 Google Inc. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// certcheck is a utility to show and check the contents of certificates.
16package main
17
18import (
19	"bytes"
20	"crypto/tls"
21	"flag"
22	"fmt"
23	"net/url"
24	"os"
25	"strings"
26
27	"github.com/golang/glog"
28	"github.com/google/certificate-transparency-go/x509"
29	"github.com/google/certificate-transparency-go/x509util"
30)
31
32var (
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	validate                  = flag.Bool("validate", false, "Validate certificate signatures")
38	timecheck                 = flag.Bool("timecheck", false, "Check current validity of certificate")
39	revokecheck               = flag.Bool("check_revocation", false, "Check revocation status of certificate")
40	ignoreUnknownCriticalExts = flag.Bool("ignore_unknown_critical_exts", false, "Ignore unknown-critical-extension errors")
41)
42
43func addCerts(filename string, pool *x509.CertPool) {
44	if filename != "" {
45		dataList, err := x509util.ReadPossiblePEMFile(filename, "CERTIFICATE")
46		if err != nil {
47			glog.Exitf("Failed to read certificate file: %v", err)
48		}
49		for _, data := range dataList {
50			certs, err := x509.ParseCertificates(data)
51			if err != nil {
52				glog.Exitf("Failed to parse certificate from %s: %v", filename, err)
53			}
54			for _, cert := range certs {
55				pool.AddCert(cert)
56			}
57		}
58	}
59}
60
61func main() {
62	flag.Parse()
63
64	failed := false
65	for _, target := range flag.Args() {
66		var err error
67		var chain []*x509.Certificate
68		if strings.HasPrefix(target, "https://") {
69			chain, err = chainFromSite(target)
70		} else {
71			chain, err = chainFromFile(target)
72		}
73		if err != nil {
74			glog.Errorf("%v", err)
75			failed = true
76			continue
77		}
78		for _, cert := range chain {
79			if *verbose {
80				fmt.Print(x509util.CertificateToString(cert))
81			}
82			if *revokecheck {
83				if err := checkRevocation(cert, *verbose); err != nil {
84					glog.Errorf("%s: certificate is revoked: %v", target, err)
85					failed = true
86				}
87			}
88		}
89		if *validate && len(chain) > 0 {
90			if *ignoreUnknownCriticalExts {
91				// We don't want failures from Verify due to unknown critical extensions,
92				// so clear them out.
93				for _, cert := range chain {
94					cert.UnhandledCriticalExtensions = nil
95				}
96			}
97			if err := validateChain(chain, *timecheck, *root, *intermediate, *useSystemRoots); err != nil {
98				glog.Errorf("%s: verification error: %v", target, err)
99				failed = true
100			}
101		}
102	}
103	if failed {
104		os.Exit(1)
105	}
106}
107
108func chainFromSite(target string) ([]*x509.Certificate, error) {
109	u, err := url.Parse(target)
110	if err != nil {
111		return nil, fmt.Errorf("%s: failed to parse URL: %v", target, err)
112	}
113	if u.Scheme != "https" {
114		return nil, fmt.Errorf("%s: non-https URL provided", target)
115	}
116	host := u.Host
117	if !strings.Contains(host, ":") {
118		host += ":443"
119	}
120
121	conn, err := tls.Dial("tcp", host, &tls.Config{InsecureSkipVerify: true})
122	if err != nil {
123		return nil, fmt.Errorf("%s: failed to dial %q: %v", target, host, err)
124	}
125	defer conn.Close()
126
127	// Convert base crypto/x509.Certificates to our forked x509.Certificate type.
128	goChain := conn.ConnectionState().PeerCertificates
129	chain := make([]*x509.Certificate, len(goChain))
130	for i, goCert := range goChain {
131		cert, err := x509.ParseCertificate(goCert.Raw)
132		if err != nil {
133			return nil, fmt.Errorf("%s: failed to convert Go Certificate [%d]: %v", target, i, err)
134		}
135		chain[i] = cert
136	}
137
138	return chain, nil
139}
140
141func chainFromFile(filename string) ([]*x509.Certificate, error) {
142	dataList, err := x509util.ReadPossiblePEMFile(filename, "CERTIFICATE")
143	if err != nil {
144		return nil, fmt.Errorf("%s: failed to read data: %v", filename, err)
145	}
146	var chain []*x509.Certificate
147	for _, data := range dataList {
148		certs, err := x509.ParseCertificates(data)
149		if x509.IsFatal(err) {
150			return nil, fmt.Errorf("%s: failed to parse: %v", filename, err)
151		}
152		if err != nil {
153			glog.Errorf("%s: non-fatal error parsing: %v", filename, err)
154		}
155		chain = append(chain, certs...)
156	}
157	return chain, nil
158}
159
160func validateChain(chain []*x509.Certificate, timecheck bool, rootsFile, intermediatesFile string, useSystemRoots bool) error {
161	roots := x509.NewCertPool()
162	if useSystemRoots {
163		systemRoots, err := x509.SystemCertPool()
164		if err != nil {
165			glog.Errorf("Failed to get system roots: %v", err)
166		}
167		roots = systemRoots
168	}
169	opts := x509.VerifyOptions{
170		KeyUsages:         []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
171		Roots:             roots,
172		Intermediates:     x509.NewCertPool(),
173		DisableTimeChecks: !timecheck,
174	}
175	addCerts(rootsFile, opts.Roots)
176	addCerts(intermediatesFile, opts.Intermediates)
177
178	if !useSystemRoots && len(rootsFile) == 0 {
179		// No root CA certs provided, so assume the chain is self-contained.
180		count := len(chain)
181		if len(chain) > 1 {
182			last := chain[len(chain)-1]
183			if bytes.Equal(last.RawSubject, last.RawIssuer) {
184				opts.Roots.AddCert(last)
185				count--
186			}
187		}
188	}
189	if len(intermediatesFile) == 0 {
190		// No intermediate CA certs provided, so assume later entries in the chain are intermediates.
191		for i := 1; i < len(chain); i++ {
192			opts.Intermediates.AddCert(chain[i])
193		}
194	}
195	_, err := chain[0].Verify(opts)
196	return err
197}
198
199func checkRevocation(cert *x509.Certificate, verbose bool) error {
200	for _, crldp := range cert.CRLDistributionPoints {
201		crlDataList, err := x509util.ReadPossiblePEMURL(crldp, "X509 CRL")
202		if err != nil {
203			glog.Errorf("failed to retrieve CRL from %q: %v", crldp, err)
204			continue
205		}
206		for _, crlData := range crlDataList {
207			crl, err := x509.ParseCertificateList(crlData)
208			if x509.IsFatal(err) {
209				glog.Errorf("failed to parse CRL from %q: %v", crldp, err)
210				continue
211			}
212			if err != nil {
213				glog.Errorf("non-fatal error parsing CRL from %q: %v", crldp, err)
214			}
215			if verbose {
216				fmt.Printf("\nRevocation data from %s:\n", crldp)
217				fmt.Print(x509util.CRLToString(crl))
218			}
219			for _, c := range crl.TBSCertList.RevokedCertificates {
220				if c.SerialNumber.Cmp(cert.SerialNumber) == 0 {
221					return fmt.Errorf("certificate is revoked since %v", c.RevocationTime)
222				}
223			}
224		}
225	}
226	return nil
227}
228