github.com/zmap/zcrypto@v0.0.0-20240512203510-0fef58d9a9db/x509/zintermediate/zintermediate.go (about)

     1  /*
     2   * ZCrypto Copyright 2017 Regents of the University of Michigan
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License"); you may not
     5   * use this file except in compliance with the License. You may obtain a copy
     6   * of the License at http://www.apache.org/licenses/LICENSE-2.0
     7   *
     8   * Unless required by applicable law or agreed to in writing, software
     9   * distributed under the License is distributed on an "AS IS" BASIS,
    10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    11   * implied. See the License for the specific language governing
    12   * permissions and limitations under the License.
    13   */
    14  
    15  // ZIntermediate is a command line utility for verifying a set prospective
    16  // intermediate certificates against a root store. Given a set of root
    17  // certificates in PEM format, it can then read in a list of candidate
    18  // intermediates. Candidate certificates are verified against the root store,
    19  // and can optionally chain through any other candidate. All candidate
    20  // certificates will be stored in memory during validation.
    21  //
    22  // ZIntermediate returns any candidate certificate with a chain back to the root
    23  // store, and ignores date-related errors and extended key usage flags, meaning
    24  // ZIntermediate will return both expired intermediates and code-signing
    25  // certificates.
    26  //
    27  // While the candidate certificates can be any certificate, ZIntermediate
    28  // expects they will be intermediates. If a non-intermediate certificate (e.g. a
    29  // certificate without IsCA set to true) is input, ZIntermediate will not build
    30  // chains through it, but will output it as valid.
    31  //
    32  // Examples:
    33  //
    34  //	$ zintermediate --roots roots.pem candidates.csv > intermediates.pem
    35  package main
    36  
    37  import (
    38  	"bufio"
    39  	"encoding/base64"
    40  	"encoding/pem"
    41  	"errors"
    42  	"flag"
    43  	"fmt"
    44  	"io"
    45  	"os"
    46  	"strings"
    47  	"time"
    48  
    49  	"github.com/zmap/zcrypto/x509"
    50  
    51  	"github.com/op/go-logging"
    52  )
    53  
    54  var log = logging.MustGetLogger("")
    55  
    56  var inputFormatArg string
    57  
    58  type inputFormatType int
    59  
    60  const (
    61  	inputFormatBase64 inputFormatType = iota
    62  	inputFormatPEM
    63  	inputFormatJSON
    64  )
    65  
    66  var inputFormat inputFormatType
    67  var inputFileName, rootFileName string
    68  
    69  func init() {
    70  	flag.StringVar(&inputFormatArg, "format", "base64", "One of {base64, pem, json}")
    71  	flag.StringVar(&rootFileName, "roots", "roots.pem", "Path to root store")
    72  	flag.Parse()
    73  
    74  	if flag.NArg() < 1 {
    75  		log.Fatalf("missing filename")
    76  	}
    77  	inputFileName = flag.Arg(0)
    78  
    79  	inputFormatArg = strings.ToLower(inputFormatArg)
    80  	switch inputFormatArg {
    81  	case "base64":
    82  		inputFormat = inputFormatBase64
    83  	case "pem":
    84  		inputFormat = inputFormatPEM
    85  	case "json":
    86  		inputFormat = inputFormatJSON
    87  	default:
    88  		log.Fatalf("unknown argument for --format \"%s\", see --help", inputFormatArg)
    89  	}
    90  }
    91  
    92  func loadPEMPool(r io.Reader) (*x509.CertPool, error) {
    93  	out := x509.NewCertPool()
    94  	scanner := bufio.NewScanner(r)
    95  
    96  	scanner.Split(func(data []byte, atEOF bool) (int, []byte, error) {
    97  		block, rest := pem.Decode(data)
    98  		if block != nil {
    99  			size := len(data) - len(rest)
   100  			return size, data[:size], nil
   101  		}
   102  		return 0, nil, nil
   103  	})
   104  
   105  	for scanner.Scan() {
   106  		pemBytes := scanner.Bytes()
   107  		ok := out.AppendCertsFromPEM(pemBytes)
   108  		if !ok {
   109  			log.Errorf("could not load PEM: %s", scanner.Text())
   110  			return nil, errors.New("unable to load PEM")
   111  		}
   112  	}
   113  	return out, nil
   114  }
   115  
   116  func loadBase64Pool(r io.Reader) (*x509.CertPool, error) {
   117  	out := x509.NewCertPool()
   118  	scanner := bufio.NewScanner(r)
   119  	scanner.Buffer(nil, 1024*1024*10)
   120  	var lines int
   121  	for lines = 0; scanner.Scan(); lines += 1 {
   122  		line := scanner.Text()
   123  		raw, err := base64.StdEncoding.DecodeString(line)
   124  		if err != nil {
   125  			log.Errorf("could not read base64: %s", line)
   126  			return nil, err
   127  		}
   128  		c, err := x509.ParseCertificate(raw)
   129  		if err != nil {
   130  			log.Errorf("could not read certificate %s: %s", line, err)
   131  			continue
   132  		}
   133  		out.AddCert(c)
   134  	}
   135  	if err := scanner.Err(); err != nil {
   136  		log.Fatalf("%s", err)
   137  	}
   138  	log.Infof("read %d lines", lines)
   139  	return out, nil
   140  }
   141  
   142  func main() {
   143  	log.Infof("loading roots from %s", rootFileName)
   144  	rootFile, err := os.Open(rootFileName)
   145  	if err != nil {
   146  		log.Fatalf("could not open %s: %s", rootFileName, err)
   147  	}
   148  	rootPool, err := loadPEMPool(rootFile)
   149  	rootFile.Close()
   150  	if err != nil {
   151  		log.Fatalf("could not load roots: %s", err)
   152  	}
   153  
   154  	log.Infof("loading candidate intermediates from %s", inputFileName)
   155  	intermediateFile, err := os.Open(inputFileName)
   156  	if err != nil {
   157  		log.Fatalf("could not open %s: %s", inputFileName, err)
   158  	}
   159  	log.Infof("using input format type %s", inputFormatArg)
   160  	var candidatePool *x509.CertPool
   161  	switch inputFormat {
   162  	case inputFormatPEM:
   163  		candidatePool, err = loadPEMPool(intermediateFile)
   164  	case inputFormatBase64:
   165  		candidatePool, err = loadBase64Pool(intermediateFile)
   166  	default:
   167  		err = fmt.Errorf("unimplemented input type: %s", inputFormatArg)
   168  	}
   169  	intermediateFile.Close()
   170  
   171  	if err != nil {
   172  		log.Fatalf("could not load candidate intermediates: %s", err)
   173  	}
   174  	candidates := candidatePool.Certificates()
   175  	log.Infof("loaded %d candidates", len(candidates))
   176  
   177  	now := time.Now()
   178  	verifyOpts := x509.VerifyOptions{
   179  		Roots:         rootPool,
   180  		Intermediates: candidatePool,
   181  		CurrentTime:   now,
   182  		KeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
   183  	}
   184  
   185  	log.Infof("validating candidates")
   186  	intermediates := make([]*x509.Certificate, 0)
   187  	rejected := 0
   188  	for idx, candidate := range candidates {
   189  		if idx > 0 && idx%1000 == 0 {
   190  			log.Infof("checked %d candidates", idx)
   191  		}
   192  		if current, expired, never, _ := candidate.Verify(verifyOpts); len(current) > 0 || len(expired) > 0 || len(never) > 0 {
   193  			intermediates = append(intermediates, candidate)
   194  			continue
   195  		}
   196  		rejected++
   197  	}
   198  	log.Infof("validation complete")
   199  	log.Infof("found %d intermediates", len(intermediates))
   200  	log.Infof("rejected %d candidates", rejected)
   201  
   202  	log.Infof("outputing intermediates")
   203  	for _, c := range intermediates {
   204  		block := pem.Block{
   205  			Type:  "CERTIFICATE",
   206  			Bytes: c.Raw,
   207  		}
   208  		pem.Encode(os.Stdout, &block)
   209  	}
   210  	log.Infof("complete")
   211  }