/*
 * ZCrypto Copyright 2017 Regents of the University of Michigan
 *
 * 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.
 */

// ZIntermediate is a command line utility for verifying a set prospective
// intermediate certificates against a root store. Given a set of root
// certificates in PEM format, it can then read in a list of candidate
// intermediates. Candidate certificates are verified against the root store,
// and can optionally chain through any other candidate. All candidate
// certificates will be stored in memory during validation.
//
// ZIntermediate returns any candidate certificate with a chain back to the root
// store, and ignores date-related errors and extended key usage flags, meaning
// ZIntermediate will return both expired intermediates and code-signing
// certificates.
//
// While the candidate certificates can be any certificate, ZIntermediate
// expects they will be intermediates. If a non-intermediate certificate (e.g. a
// certificate without IsCA set to true) is input, ZIntermediate will not build
// chains through it, but will output it as valid.
//
// Examples:
//
//	$ zintermediate --roots roots.pem candidates.csv > intermediates.pem
package main

import (
	"bufio"
	"encoding/base64"
	"encoding/pem"
	"errors"
	"flag"
	"fmt"
	"io"
	"os"
	"strings"
	"time"

	"github.com/zmap/zcrypto/x509"

	"github.com/op/go-logging"
)

var log = logging.MustGetLogger("")

var inputFormatArg string

type inputFormatType int

const (
	inputFormatBase64 inputFormatType = iota
	inputFormatPEM
	inputFormatJSON
)

var inputFormat inputFormatType
var inputFileName, rootFileName string

func init() {
	flag.StringVar(&inputFormatArg, "format", "base64", "One of {base64, pem, json}")
	flag.StringVar(&rootFileName, "roots", "roots.pem", "Path to root store")
	flag.Parse()

	if flag.NArg() < 1 {
		log.Fatalf("missing filename")
	}
	inputFileName = flag.Arg(0)

	inputFormatArg = strings.ToLower(inputFormatArg)
	switch inputFormatArg {
	case "base64":
		inputFormat = inputFormatBase64
	case "pem":
		inputFormat = inputFormatPEM
	case "json":
		inputFormat = inputFormatJSON
	default:
		log.Fatalf("unknown argument for --format \"%s\", see --help", inputFormatArg)
	}
}

func loadPEMPool(r io.Reader) (*x509.CertPool, error) {
	out := x509.NewCertPool()
	scanner := bufio.NewScanner(r)

	scanner.Split(func(data []byte, atEOF bool) (int, []byte, error) {
		block, rest := pem.Decode(data)
		if block != nil {
			size := len(data) - len(rest)
			return size, data[:size], nil
		}
		return 0, nil, nil
	})

	for scanner.Scan() {
		pemBytes := scanner.Bytes()
		ok := out.AppendCertsFromPEM(pemBytes)
		if !ok {
			log.Errorf("could not load PEM: %s", scanner.Text())
			return nil, errors.New("unable to load PEM")
		}
	}
	return out, nil
}

func loadBase64Pool(r io.Reader) (*x509.CertPool, error) {
	out := x509.NewCertPool()
	scanner := bufio.NewScanner(r)
	scanner.Buffer(nil, 1024*1024*10)
	var lines int
	for lines = 0; scanner.Scan(); lines += 1 {
		line := scanner.Text()
		raw, err := base64.StdEncoding.DecodeString(line)
		if err != nil {
			log.Errorf("could not read base64: %s", line)
			return nil, err
		}
		c, err := x509.ParseCertificate(raw)
		if err != nil {
			log.Errorf("could not read certificate %s: %s", line, err)
			continue
		}
		out.AddCert(c)
	}
	if err := scanner.Err(); err != nil {
		log.Fatalf("%s", err)
	}
	log.Infof("read %d lines", lines)
	return out, nil
}

func main() {
	log.Infof("loading roots from %s", rootFileName)
	rootFile, err := os.Open(rootFileName)
	if err != nil {
		log.Fatalf("could not open %s: %s", rootFileName, err)
	}
	rootPool, err := loadPEMPool(rootFile)
	rootFile.Close()
	if err != nil {
		log.Fatalf("could not load roots: %s", err)
	}

	log.Infof("loading candidate intermediates from %s", inputFileName)
	intermediateFile, err := os.Open(inputFileName)
	if err != nil {
		log.Fatalf("could not open %s: %s", inputFileName, err)
	}
	log.Infof("using input format type %s", inputFormatArg)
	var candidatePool *x509.CertPool
	switch inputFormat {
	case inputFormatPEM:
		candidatePool, err = loadPEMPool(intermediateFile)
	case inputFormatBase64:
		candidatePool, err = loadBase64Pool(intermediateFile)
	default:
		err = fmt.Errorf("unimplemented input type: %s", inputFormatArg)
	}
	intermediateFile.Close()

	if err != nil {
		log.Fatalf("could not load candidate intermediates: %s", err)
	}
	candidates := candidatePool.Certificates()
	log.Infof("loaded %d candidates", len(candidates))

	now := time.Now()
	verifyOpts := x509.VerifyOptions{
		Roots:         rootPool,
		Intermediates: candidatePool,
		CurrentTime:   now,
		KeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
	}

	log.Infof("validating candidates")
	intermediates := make([]*x509.Certificate, 0)
	rejected := 0
	for idx, candidate := range candidates {
		if idx > 0 && idx%1000 == 0 {
			log.Infof("checked %d candidates", idx)
		}
		if current, expired, never, _ := candidate.Verify(verifyOpts); len(current) > 0 || len(expired) > 0 || len(never) > 0 {
			intermediates = append(intermediates, candidate)
			continue
		}
		rejected++
	}
	log.Infof("validation complete")
	log.Infof("found %d intermediates", len(intermediates))
	log.Infof("rejected %d candidates", rejected)

	log.Infof("outputing intermediates")
	for _, c := range intermediates {
		block := pem.Block{
			Type:  "CERTIFICATE",
			Bytes: c.Raw,
		}
		pem.Encode(os.Stdout, &block)
	}
	log.Infof("complete")
}
