1
2
3
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
35
36 rejectHosts map[string]bool
37
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
65
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
83
84
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
99
100
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
193 UserAgent string
194
195 Addr string
196
197
198
199 PrivKey string
200
201
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
232
233
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