1package main
2
3import (
4 "acme-mock/acme"
5 "bytes"
6 "crypto/rand"
7 "crypto/rsa"
8 "crypto/x509"
9 "crypto/x509/pkix"
10 "encoding/base64"
11 "encoding/json"
12 "encoding/pem"
13 "flag"
14 "fmt"
15 "io/ioutil"
16 "log"
17 "math/big"
18 "net/http"
19 "os"
20 "path"
21 "strconv"
22 "sync"
23 "time"
24)
25
26////
27// Types
28////
29
30type acmeFn func(http.ResponseWriter, *http.Request) interface{}
31
32type orderCtx struct {
33 obj *acme.Order
34 crt []byte
35}
36
37type jwsobj struct {
38 Protected string `json:"protected"`
39 Payload string `json:"payload"`
40 Signature string `json:"signature"`
41}
42
43type authzResponse struct {
44 Status string `json:"status"`
45 Expires string `json:"expires"`
46 Identifier Identifier `json:"identifier"`
47 Challenges []Challenge `json:"challenges"`
48}
49
50type Identifier struct {
51 Type string `json:"type"`
52 Value string `json:"value"`
53}
54
55type Challenge struct {
56 Type string `json:"type"`
57 URL string `json:"url"`
58 Status string `json:"status"`
59 Validated string `json:"validated"`
60 Token string `json:"token"`
61}
62
63////
64// Variables & Constants
65////
66
67const (
68 directoryPath = "/directory"
69 newNoncePath = "/new-nonce"
70 newAccountPath = "/new-account"
71 newOrderPath = "/new-order"
72 revokeCertPath = "/revoke-cert"
73 keyChangePath = "/key-change"
74 authzPath = "/authz"
75
76 finalizePath = "/finalize/"
77 certificatePath = "/certificate/"
78 orderPath = "/order/"
79
80 replayNonce = "oFvnlFP1wIhRlYS2jTaXbA"
81)
82
83var (
84 httpsAddr = flag.String("a", ":443", "address used for HTTPS socket")
85 tlsKey = flag.String("k", "", "TLS private key")
86 tlsCert = flag.String("c", "", "TLS certificate")
87 rsaBits = flag.Int("b", 2048, "RSA key size")
88)
89
90var key *rsa.PrivateKey
91var caKey *rsa.PrivateKey
92var orders []*orderCtx
93var ordersMtx sync.Mutex
94
95////
96// Utility functions
97////
98
99func createCrt(csrMsg *acme.CSRMessage) ([]byte, error) {
100 data, err := base64.RawURLEncoding.DecodeString(csrMsg.Csr)
101 if err != nil {
102 return nil, err
103 }
104
105 csr, err := x509.ParseCertificateRequest(data)
106 if err != nil {
107 return nil, err
108 }
109
110 caTemp := x509.Certificate{
111 SerialNumber: big.NewInt(1),
112 Subject: pkix.Name{
113 Organization: []string{"Mock CA"},
114 },
115 NotBefore: time.Now(),
116 NotAfter: time.Now().AddDate(0, 0, 1), // Valid for 1 day
117 KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
118 BasicConstraintsValid: true,
119 IsCA: true,
120 }
121
122 caCert, _ := x509.CreateCertificate(rand.Reader, &caTemp, &caTemp, &caKey.PublicKey, key)
123
124 certTemp := x509.Certificate{
125 SerialNumber: big.NewInt(5),
126 Subject: csr.Subject,
127 DNSNames: csr.DNSNames,
128 EmailAddresses: csr.EmailAddresses,
129 IPAddresses: csr.IPAddresses,
130 NotBefore: time.Now(),
131 NotAfter: time.Now().AddDate(0, 0, 1), // Valid for 1 day
132 }
133
134 crt, err := x509.CreateCertificate(rand.Reader, &certTemp, &caTemp, &key.PublicKey, caKey)
135
136 // Encode the certificates to PEM format
137 pemCert1 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert})
138 pemCert2 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: crt})
139
140 // Concatenate the PEM-encoded certificates
141 fullChain := append(pemCert1, pemCert2...)
142
143 return fullChain, err
144}
145
146func getOrder(r *http.Request) (*orderCtx, error) {
147 id, err := strconv.Atoi(path.Base(r.URL.Path))
148 if err != nil {
149 return nil, err
150 }
151
152 ordersMtx.Lock()
153 defer ordersMtx.Unlock()
154
155 if id < len(orders) {
156 return orders[id], nil
157 } else {
158 return nil, fmt.Errorf("order not found")
159 }
160}
161
162func createURL(r *http.Request, path string) string {
163 r.URL.Host = r.Host
164 r.URL.Scheme = "https"
165 r.URL.Path = path
166
167 return r.URL.String()
168}
169
170////
171// Handlers
172////
173
174func directoryHandler(w http.ResponseWriter, r *http.Request) interface{} {
175
176 return acme.Directory{
177 NewNonceURL: createURL(r, newNoncePath),
178 NewAccountURL: createURL(r, newAccountPath),
179 NewOrderURL: createURL(r, newOrderPath),
180 RevokeCertURL: createURL(r, revokeCertPath),
181 KeyChangeURL: createURL(r, keyChangePath),
182 NewAuthzURL: createURL(r, authzPath),
183 }
184}
185
186func nonceHandler(w http.ResponseWriter, r *http.Request) {
187 // Hardcoded value copied from RFC 8555
188 w.Header().Add("Replay-Nonce", replayNonce)
189
190 w.Header().Add("Cache-Control", "no-store")
191 w.WriteHeader(http.StatusOK)
192}
193
194func accountHandler(w http.ResponseWriter, r *http.Request) interface{} {
195 return acme.Account{
196 Status: acme.StatusValid,
197 Orders: createURL(r, "orders"),
198 }
199}
200
201func newOrderHandler(w http.ResponseWriter, r *http.Request) interface{} {
202 var order acme.Order
203 err := json.NewDecoder(r.Body).Decode(&order)
204 if err != nil {
205 http.Error(w, "Bad Request", http.StatusBadRequest)
206 return nil
207 }
208
209 ordersMtx.Lock()
210 orderId := strconv.Itoa(len(orders))
211 orders = append(orders, &orderCtx{&order, nil})
212 ordersMtx.Unlock()
213
214 order.Finalize = createURL(r, path.Join(finalizePath, orderId))
215
216 mockChallengeURL := "https://localhost:8443/authz"
217 order.Authorizations = []string{mockChallengeURL}
218
219 orderURL := createURL(r, path.Join(orderPath, orderId))
220 w.Header().Add("Location", orderURL)
221 w.Header().Add("Replay-Nonce", replayNonce)
222
223 w.WriteHeader(http.StatusCreated)
224 return order
225}
226
227func finalizeHandler(w http.ResponseWriter, r *http.Request) interface{} {
228 id := path.Base(r.URL.Path)
229 order, err := getOrder(r)
230 if err != nil {
231 http.Error(w, "Not Found", http.StatusNotFound)
232 return nil
233 }
234
235 var csrMsg acme.CSRMessage
236 err = json.NewDecoder(r.Body).Decode(&csrMsg)
237 if err != nil {
238 http.Error(w, "Invalid JSON", http.StatusBadRequest)
239 return nil
240 }
241
242 order.crt, err = createCrt(&csrMsg)
243 if err != nil {
244 http.Error(w, "createCrt failed", http.StatusInternalServerError)
245 return nil
246 }
247
248 order.obj.Status = acme.StatusValid
249 order.obj.Certificate = createURL(r, path.Join(certificatePath, id))
250
251 orderURL := createURL(r, path.Join(orderPath, id))
252 w.Header().Add("Location", orderURL)
253 w.Header().Add("Replay-Nonce", replayNonce)
254
255 return order.obj
256}
257
258func orderHandler(w http.ResponseWriter, r *http.Request) interface{} {
259 order, err := getOrder(r)
260 w.Header().Add("Replay-Nonce", replayNonce)
261
262 if err != nil {
263 http.Error(w, "Not Found", http.StatusNotFound)
264 return nil
265 }
266
267 return order.obj
268}
269
270func certHandler(w http.ResponseWriter, r *http.Request) {
271 w.Header().Set("Replay-Nonce", replayNonce)
272
273 order, err := getOrder(r)
274 if err != nil {
275 http.Error(w, "Not Found", http.StatusNotFound)
276 return
277 }
278
279 w.Write(order.crt)
280}
281
282////
283// Middleware
284////
285
286func authzHandler(w http.ResponseWriter, r *http.Request) interface{} {
287 // Get the current date and time
288 currentTime := time.Now().UTC()
289
290 // Format the current date and time as strings
291 currentTimeString := currentTime.Format(time.RFC3339)
292
293 // Create a authzResponse object
294 resp := authzResponse{
295 Status: "valid",
296 Expires: currentTimeString,
297 Identifier: Identifier{
298 Type: "dns",
299 Value: "localhost",
300 },
301 Challenges: []Challenge{
302 {
303 Type: "http-01",
304 URL: createURL(r, "/chall"),
305 Token: replayNonce,
306 Status: "valid",
307 Validated: currentTimeString,
308 },
309 },
310 }
311
312 // Set the Content-Type header
313 w.Header().Set("Content-Type", "application/json")
314 w.Header().Set("Replay-Nonce", replayNonce)
315
316 // Set the status code to 201
317 w.WriteHeader(http.StatusCreated)
318
319 return resp
320}
321
322func jsonMiddleware(fn acmeFn) http.Handler {
323 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
324 w.Header().Add("Content-Type", "application/json")
325
326 val := fn(w, r)
327 if val == nil {
328 return
329 }
330
331 err := json.NewEncoder(w).Encode(val)
332 if err != nil {
333 http.Error(w, "JSON encoding failed", http.StatusInternalServerError)
334 return
335 }
336 })
337}
338
339func jsonMiddlewareNewAccount(fn acmeFn) http.Handler {
340 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
341 w.Header().Add("Content-Type", "application/json")
342 w.Header().Add("Location", createURL(r, "new-account"))
343 w.Header().Add("Replay-Nonce", replayNonce)
344 w.WriteHeader(http.StatusCreated)
345
346 val := fn(w, r)
347 if val == nil {
348 return
349 }
350
351 err := json.NewEncoder(w).Encode(val)
352 if err != nil {
353 http.Error(w, "JSON encoding failed", http.StatusInternalServerError)
354 return
355 }
356 })
357}
358
359func jwtMiddleware(h http.Handler) http.Handler {
360 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
361 var jws jwsobj
362 err := json.NewDecoder(r.Body).Decode(&jws)
363 if err != nil {
364 http.Error(w, "Invalid JSON", http.StatusBadRequest)
365 return
366 }
367
368 payload, err := base64.RawURLEncoding.DecodeString(jws.Payload)
369 if err != nil {
370 http.Error(w, "Invalid Base64", http.StatusBadRequest)
371 return
372 }
373
374 r.Body = ioutil.NopCloser(bytes.NewReader(payload))
375 h.ServeHTTP(w, r)
376 })
377}
378
379////
380// main
381////
382
383func main() {
384 flag.Parse()
385 if *tlsKey == "" || *tlsCert == "" {
386 fmt.Fprintf(flag.CommandLine.Output(), "missing TLS key or certificate\n")
387 flag.Usage()
388 os.Exit(2)
389 }
390
391 var err error
392 key, err = rsa.GenerateKey(rand.Reader, *rsaBits)
393 caKey, err = rsa.GenerateKey(rand.Reader, *rsaBits)
394 if err != nil {
395 log.Fatal(err)
396 }
397
398 http.Handle(directoryPath, jsonMiddleware(directoryHandler))
399 http.HandleFunc(newNoncePath, nonceHandler)
400 http.Handle(newAccountPath, jsonMiddlewareNewAccount(accountHandler))
401 http.Handle(newOrderPath, jwtMiddleware(jsonMiddleware(newOrderHandler)))
402 http.Handle(finalizePath, jwtMiddleware(jsonMiddleware(finalizeHandler)))
403 http.HandleFunc(certificatePath, certHandler)
404 http.Handle(orderPath, jsonMiddleware(orderHandler))
405 http.Handle(authzPath, jsonMiddleware(authzHandler))
406 log.Fatal(http.ListenAndServeTLS(*httpsAddr, *tlsCert, *tlsKey, nil))
407}