acme-mock

An ACME server implementation performing no validations

git clone https://git.8pit.net/acme-mock.git

  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}