...

Source file src/github.com/letsencrypt/boulder/test/ct-test-srv/main.go

Documentation: github.com/letsencrypt/boulder/test/ct-test-srv

     1  // This is a test server that implements the subset of RFC6962 APIs needed to
     2  // run Boulder's CT log submission code. Currently it only implements add-chain.
     3  // This is used by startservers.py.
     4  package main
     5  
     6  import (
     7  	"crypto/ecdsa"
     8  	"crypto/sha256"
     9  	"crypto/x509"
    10  	"encoding/base64"
    11  	"encoding/json"
    12  	"flag"
    13  	"fmt"
    14  	"io"
    15  	"log"
    16  	"math/rand"
    17  	"net/http"
    18  	"os"
    19  	"strings"
    20  	"sync"
    21  	"time"
    22  
    23  	"github.com/letsencrypt/boulder/cmd"
    24  	"github.com/letsencrypt/boulder/publisher"
    25  )
    26  
    27  type ctSubmissionRequest struct {
    28  	Chain []string `json:"chain"`
    29  }
    30  
    31  type integrationSrv struct {
    32  	sync.Mutex
    33  	submissions map[string]int64
    34  	// Hostnames where we refuse to provide an SCT. This is to exercise the code
    35  	// path where all CT servers fail.
    36  	rejectHosts map[string]bool
    37  	// A list of entries that we rejected based on rejectHosts.
    38  	rejected      []string
    39  	key           *ecdsa.PrivateKey
    40  	flakinessRate int
    41  	userAgent     string
    42  }
    43  
    44  func readJSON(r *http.Request, output interface{}) error {
    45  	if r.Method != "POST" {
    46  		return fmt.Errorf("incorrect method; only POST allowed")
    47  	}
    48  	bodyBytes, err := io.ReadAll(r.Body)
    49  	if err != nil {
    50  		return err
    51  	}
    52  
    53  	err = json.Unmarshal(bodyBytes, output)
    54  	if err != nil {
    55  		return err
    56  	}
    57  	return nil
    58  }
    59  
    60  func (is *integrationSrv) addChain(w http.ResponseWriter, r *http.Request) {
    61  	is.addChainOrPre(w, r, false)
    62  }
    63  
    64  // addRejectHost takes a JSON POST with a "host" field; any subsequent
    65  // submissions for that host will get a 400 error.
    66  func (is *integrationSrv) addRejectHost(w http.ResponseWriter, r *http.Request) {
    67  	var rejectHostReq struct {
    68  		Host string
    69  	}
    70  	err := readJSON(r, &rejectHostReq)
    71  	if err != nil {
    72  		http.Error(w, err.Error(), http.StatusBadRequest)
    73  		return
    74  	}
    75  
    76  	is.Lock()
    77  	defer is.Unlock()
    78  	is.rejectHosts[rejectHostReq.Host] = true
    79  	w.Write([]byte{})
    80  }
    81  
    82  // getRejections returns a JSON array containing strings; those strings are
    83  // base64 encodings of certificates or precertificates that were rejected due to
    84  // the rejectHosts mechanism.
    85  func (is *integrationSrv) getRejections(w http.ResponseWriter, r *http.Request) {
    86  	is.Lock()
    87  	defer is.Unlock()
    88  	output, err := json.Marshal(is.rejected)
    89  	if err != nil {
    90  		http.Error(w, err.Error(), http.StatusBadRequest)
    91  		return
    92  	}
    93  
    94  	w.WriteHeader(http.StatusOK)
    95  	w.Write(output)
    96  }
    97  
    98  // shouldReject checks if the given host is in the rejectHosts list for the
    99  // integrationSrv. If it is, then the chain is appended to the integrationSrv
   100  // rejected list and true is returned indicating the request should be rejected.
   101  func (is *integrationSrv) shouldReject(host, chain string) bool {
   102  	is.Lock()
   103  	defer is.Unlock()
   104  	if is.rejectHosts[host] {
   105  		is.rejected = append(is.rejected, chain)
   106  		return true
   107  	}
   108  	return false
   109  }
   110  
   111  func (is *integrationSrv) addPreChain(w http.ResponseWriter, r *http.Request) {
   112  	is.addChainOrPre(w, r, true)
   113  }
   114  
   115  func (is *integrationSrv) addChainOrPre(w http.ResponseWriter, r *http.Request, precert bool) {
   116  	if is.userAgent != "" && r.UserAgent() != is.userAgent {
   117  		http.Error(w, "invalid user-agent", http.StatusBadRequest)
   118  		return
   119  	}
   120  	if r.Method != "POST" {
   121  		http.NotFound(w, r)
   122  		return
   123  	}
   124  	bodyBytes, err := io.ReadAll(r.Body)
   125  	if err != nil {
   126  		http.Error(w, err.Error(), http.StatusBadRequest)
   127  		return
   128  	}
   129  
   130  	var addChainReq ctSubmissionRequest
   131  	err = json.Unmarshal(bodyBytes, &addChainReq)
   132  	if err != nil {
   133  		http.Error(w, err.Error(), http.StatusBadRequest)
   134  		return
   135  	}
   136  	if len(addChainReq.Chain) == 0 {
   137  		w.WriteHeader(400)
   138  		return
   139  	}
   140  
   141  	b, err := base64.StdEncoding.DecodeString(addChainReq.Chain[0])
   142  	if err != nil {
   143  		w.WriteHeader(400)
   144  		return
   145  	}
   146  	cert, err := x509.ParseCertificate(b)
   147  	if err != nil {
   148  		w.WriteHeader(400)
   149  		return
   150  	}
   151  	hostnames := strings.Join(cert.DNSNames, ",")
   152  
   153  	for _, h := range cert.DNSNames {
   154  		if is.shouldReject(h, addChainReq.Chain[0]) {
   155  			w.WriteHeader(400)
   156  			return
   157  		}
   158  	}
   159  
   160  	is.Lock()
   161  	is.submissions[hostnames]++
   162  	is.Unlock()
   163  
   164  	if is.flakinessRate != 0 && rand.Intn(100) < is.flakinessRate {
   165  		time.Sleep(10 * time.Second)
   166  	}
   167  
   168  	w.WriteHeader(http.StatusOK)
   169  	w.Write(publisher.CreateTestingSignedSCT(addChainReq.Chain, is.key, precert, time.Now()))
   170  }
   171  
   172  func (is *integrationSrv) getSubmissions(w http.ResponseWriter, r *http.Request) {
   173  	if r.Method != "GET" {
   174  		http.NotFound(w, r)
   175  		return
   176  	}
   177  
   178  	is.Lock()
   179  	hostnames := r.URL.Query().Get("hostnames")
   180  	submissions := is.submissions[hostnames]
   181  	is.Unlock()
   182  
   183  	w.WriteHeader(http.StatusOK)
   184  	fmt.Fprintf(w, "%d", submissions)
   185  }
   186  
   187  type config struct {
   188  	Personalities []Personality
   189  }
   190  
   191  type Personality struct {
   192  	// If present, the expected UserAgent of the reporter to this test CT log.
   193  	UserAgent string
   194  	// Port (and optionally IP) to listen on
   195  	Addr string
   196  	// Private key for signing SCTs
   197  	// Generate your own with:
   198  	// openssl ecparam -name prime256v1 -genkey -outform der -noout | base64 -w 0
   199  	PrivKey string
   200  	// FlakinessRate is an integer between 0-100 that controls how often the log
   201  	// "flakes", i.e. fails to respond in a reasonable time frame.
   202  	FlakinessRate int
   203  }
   204  
   205  func runPersonality(p Personality) {
   206  	keyDER, err := base64.StdEncoding.DecodeString(p.PrivKey)
   207  	if err != nil {
   208  		log.Fatal(err)
   209  	}
   210  	key, err := x509.ParseECPrivateKey(keyDER)
   211  	if err != nil {
   212  		log.Fatal(err)
   213  	}
   214  	pubKeyBytes, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
   215  	if err != nil {
   216  		log.Fatal(err)
   217  	}
   218  	is := integrationSrv{
   219  		key:           key,
   220  		flakinessRate: p.FlakinessRate,
   221  		submissions:   make(map[string]int64),
   222  		rejectHosts:   make(map[string]bool),
   223  		userAgent:     p.UserAgent,
   224  	}
   225  	m := http.NewServeMux()
   226  	m.HandleFunc("/submissions", is.getSubmissions)
   227  	m.HandleFunc("/ct/v1/add-pre-chain", is.addPreChain)
   228  	m.HandleFunc("/ct/v1/add-chain", is.addChain)
   229  	m.HandleFunc("/add-reject-host", is.addRejectHost)
   230  	m.HandleFunc("/get-rejections", is.getRejections)
   231  	// The gosec linter complains that ReadHeaderTimeout is not set. That's fine,
   232  	// because this is test-only code.
   233  	////nolint:gosec
   234  	srv := &http.Server{
   235  		Addr:    p.Addr,
   236  		Handler: m,
   237  	}
   238  	logID := sha256.Sum256(pubKeyBytes)
   239  	log.Printf("ct-test-srv on %s with pubkey %s and log ID %s", p.Addr,
   240  		base64.StdEncoding.EncodeToString(pubKeyBytes), base64.StdEncoding.EncodeToString(logID[:]))
   241  	log.Fatal(srv.ListenAndServe())
   242  }
   243  
   244  func main() {
   245  	configFile := flag.String("config", "", "Path to config file.")
   246  	flag.Parse()
   247  	data, err := os.ReadFile(*configFile)
   248  	if err != nil {
   249  		log.Fatal(err)
   250  	}
   251  	var c config
   252  	err = json.Unmarshal(data, &c)
   253  	if err != nil {
   254  		log.Fatal(err)
   255  	}
   256  
   257  	for _, p := range c.Personalities {
   258  		go runPersonality(p)
   259  	}
   260  	cmd.WaitForSignal()
   261  }
   262  

View as plain text