// Copyright 2016 Google LLC. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // certcheck is a utility to show and check the contents of certificates. package main import ( "bytes" "crypto/tls" "flag" "fmt" "net/url" "os" "strings" "github.com/google/certificate-transparency-go/x509" "github.com/google/certificate-transparency-go/x509util" "k8s.io/klog/v2" ) var ( root = flag.String("root", "", "Root CA certificate file") intermediate = flag.String("intermediate", "", "Intermediate CA certificate file") useSystemRoots = flag.Bool("system_roots", false, "Use system roots") verbose = flag.Bool("verbose", false, "Verbose output") strict = flag.Bool("strict", true, "Set non-zero exit code for non-fatal errors in parsing") validate = flag.Bool("validate", false, "Validate certificate signatures") checkTime = flag.Bool("check_time", false, "Check current validity of certificate") checkName = flag.Bool("check_name", true, "Check certificate name validity") checkEKU = flag.Bool("check_eku", true, "Check EKU nesting validity") checkPathLen = flag.Bool("check_path_len", true, "Check path len constraint validity") checkNameConstraint = flag.Bool("check_name_constraint", true, "Check name constraints") checkUnknownCriticalExts = flag.Bool("check_unknown_critical_exts", true, "Check for unknown critical extensions") checkRevoked = flag.Bool("check_revocation", false, "Check revocation status of certificate") ) func addCerts(filename string, pool *x509.CertPool) { if filename != "" { dataList, err := x509util.ReadPossiblePEMFile(filename, "CERTIFICATE") if err != nil { klog.Exitf("Failed to read certificate file: %v", err) } for _, data := range dataList { certs, err := x509.ParseCertificates(data) if err != nil { klog.Exitf("Failed to parse certificate from %s: %v", filename, err) } for _, cert := range certs { pool.AddCert(cert) } } } } func main() { klog.InitFlags(nil) flag.Parse() failed := false for _, target := range flag.Args() { var err error var chain []*x509.Certificate if strings.HasPrefix(target, "https://") { chain, err = chainFromSite(target) } else { chain, err = chainFromFile(target) } if err != nil { klog.Errorf("%v", err) } if x509.IsFatal(err) { failed = true continue } else if err != nil && *strict { failed = true } for _, cert := range chain { if *verbose { fmt.Print(x509util.CertificateToString(cert)) } if *checkRevoked { if err := checkRevocation(cert, *verbose); err != nil { klog.Errorf("%s: certificate is revoked: %v", target, err) failed = true } } } if *validate && len(chain) > 0 { opts := x509.VerifyOptions{ DisableTimeChecks: !*checkTime, DisableCriticalExtensionChecks: !*checkUnknownCriticalExts, DisableNameChecks: !*checkName, DisableEKUChecks: !*checkEKU, DisablePathLenChecks: !*checkPathLen, DisableNameConstraintChecks: !*checkNameConstraint, } if err := validateChain(chain, opts, *root, *intermediate, *useSystemRoots); err != nil { klog.Errorf("%s: verification error: %v", target, err) failed = true } } } if failed { os.Exit(1) } } // chainFromSite retrieves the certificate chain from an https: URL. // Note that both a chain and an error can be returned (in which case // the error will be of type x509.NonFatalErrors). func chainFromSite(target string) ([]*x509.Certificate, error) { u, err := url.Parse(target) if err != nil { return nil, fmt.Errorf("%s: failed to parse URL: %v", target, err) } if u.Scheme != "https" { return nil, fmt.Errorf("%s: non-https URL provided", target) } host := u.Host if !strings.Contains(host, ":") { host += ":443" } // Insecure TLS connection here so we can always proceed. conn, err := tls.Dial("tcp", host, &tls.Config{InsecureSkipVerify: true}) if err != nil { return nil, fmt.Errorf("%s: failed to dial %q: %v", target, host, err) } defer conn.Close() // Convert base crypto/x509.Certificates to our forked x509.Certificate type. goChain := conn.ConnectionState().PeerCertificates var nfe *x509.NonFatalErrors chain := make([]*x509.Certificate, len(goChain)) for i, goCert := range goChain { cert, err := x509.ParseCertificate(goCert.Raw) if x509.IsFatal(err) { return nil, fmt.Errorf("%s: failed to convert Go Certificate [%d]: %v", target, i, err) } else if errs, ok := err.(x509.NonFatalErrors); ok { nfe = nfe.Append(&errs) } else if err != nil { return nil, fmt.Errorf("%s: failed to convert Go Certificate [%d]: %v", target, i, err) } chain[i] = cert } if nfe.HasError() { return chain, *nfe } return chain, nil } // chainFromSite retrieves a certificate chain from a PEM file. // Note that both a chain and an error can be returned (in which case // the error will be of type x509.NonFatalErrors). func chainFromFile(filename string) ([]*x509.Certificate, error) { dataList, err := x509util.ReadPossiblePEMFile(filename, "CERTIFICATE") if err != nil { return nil, fmt.Errorf("%s: failed to read data: %v", filename, err) } var nfe *x509.NonFatalErrors var chain []*x509.Certificate for _, data := range dataList { certs, err := x509.ParseCertificates(data) if x509.IsFatal(err) { return nil, fmt.Errorf("%s: failed to parse: %v", filename, err) } else if errs, ok := err.(x509.NonFatalErrors); ok { nfe = nfe.Append(&errs) } else if err != nil { return nil, fmt.Errorf("%s: failed to parse: %v", filename, err) } chain = append(chain, certs...) } if nfe.HasError() { return chain, *nfe } return chain, nil } func validateChain(chain []*x509.Certificate, opts x509.VerifyOptions, rootsFile, intermediatesFile string, useSystemRoots bool) error { roots := x509.NewCertPool() if useSystemRoots { systemRoots, err := x509.SystemCertPool() if err != nil { klog.Errorf("Failed to get system roots: %v", err) } roots = systemRoots } opts.KeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageAny} opts.Roots = roots opts.Intermediates = x509.NewCertPool() addCerts(rootsFile, opts.Roots) addCerts(intermediatesFile, opts.Intermediates) if !useSystemRoots && len(rootsFile) == 0 { // No root CA certs provided, so assume the chain is self-contained. if len(chain) > 1 { last := chain[len(chain)-1] if bytes.Equal(last.RawSubject, last.RawIssuer) { opts.Roots.AddCert(last) } } } if len(intermediatesFile) == 0 { // No intermediate CA certs provided, so assume later entries in the chain are intermediates. for i := 1; i < len(chain); i++ { opts.Intermediates.AddCert(chain[i]) } } _, err := chain[0].Verify(opts) return err } func checkRevocation(cert *x509.Certificate, verbose bool) error { for _, crldp := range cert.CRLDistributionPoints { crlDataList, err := x509util.ReadPossiblePEMURL(crldp, "X509 CRL") if err != nil { klog.Errorf("failed to retrieve CRL from %q: %v", crldp, err) continue } for _, crlData := range crlDataList { crl, err := x509.ParseCertificateList(crlData) if x509.IsFatal(err) { klog.Errorf("failed to parse CRL from %q: %v", crldp, err) continue } if err != nil { klog.Errorf("non-fatal error parsing CRL from %q: %v", crldp, err) } if verbose { fmt.Printf("\nRevocation data from %s:\n", crldp) fmt.Print(x509util.CRLToString(crl)) } for _, c := range crl.TBSCertList.RevokedCertificates { if c.SerialNumber.Cmp(cert.SerialNumber) == 0 { return fmt.Errorf("certificate is revoked since %v", c.RevocationTime) } } } } return nil }