1package main
2
3import (
4 "bytes"
5 "crypto/rand"
6 "crypto/rsa"
7 "crypto/x509"
8 "encoding/base64"
9 "encoding/json"
10 "encoding/pem"
11 "flag"
12 "fmt"
13 "github.com/nmeum/acme-mock/acme"
14 "io/ioutil"
15 "log"
16 "math/big"
17 "net/http"
18 "os"
19 "path"
20 "strconv"
21 "sync"
22)
23
24////
25// Types
26////
27
28type acmeFn func(http.ResponseWriter, *http.Request) interface{}
29
30type orderCtx struct {
31 obj *acme.Order
32 crt []byte
33}
34
35type jwsobj struct {
36 Protected string `json:"protected"`
37 Payload string `json:"payload"`
38 Signature string `json:"signature"`
39}
40
41////
42// Variables & Constants
43////
44
45const (
46 directoryPath = "/directory"
47 newNoncePath = "/new-nonce"
48 newAccountPath = "/new-account"
49 newOrderPath = "/new-order"
50 revokeCertPath = "/revoke-cert"
51 keyChangePath = "/key-change"
52
53 finalizePath = "/finalize/"
54 certificatePath = "/certificate/"
55 orderPath = "/order/"
56)
57
58var (
59 httpsAddr = flag.String("a", ":443", "address used for HTTPS socket")
60 tlsKey = flag.String("k", "", "TLS private key")
61 tlsCert = flag.String("c", "", "TLS certificate")
62 rsaBits = flag.Int("b", 2048, "RSA key size")
63)
64
65var key *rsa.PrivateKey
66var orders []*orderCtx
67var ordersMtx sync.Mutex
68
69////
70// Utility functions
71////
72
73func createCrt(csrMsg *acme.CSRMessage) ([]byte, error) {
74 data, err := base64.RawURLEncoding.DecodeString(csrMsg.Csr)
75 if err != nil {
76 return nil, err
77 }
78
79 csr, err := x509.ParseCertificateRequest(data)
80 if err != nil {
81 return nil, err
82 }
83
84 temp := x509.Certificate{
85 SerialNumber: big.NewInt(5),
86 Subject: csr.Subject,
87 DNSNames: csr.DNSNames,
88 EmailAddresses: csr.EmailAddresses,
89 IPAddresses: csr.IPAddresses,
90 }
91
92 return x509.CreateCertificate(rand.Reader, &temp, &temp, &key.PublicKey, key)
93}
94
95func getOrder(r *http.Request) (*orderCtx, error) {
96 id, err := strconv.Atoi(path.Base(r.URL.Path))
97 if err != nil {
98 return nil, err
99 }
100
101 ordersMtx.Lock()
102 defer ordersMtx.Unlock()
103
104 if id < len(orders) {
105 return orders[id], nil
106 } else {
107 return nil, fmt.Errorf("order not found")
108 }
109}
110
111func createURL(r *http.Request, path string) string {
112 r.URL.Host = r.Host
113 r.URL.Scheme = "https"
114 r.URL.Path = path
115
116 return r.URL.String()
117}
118
119////
120// Handlers
121////
122
123func directoryHandler(w http.ResponseWriter, r *http.Request) interface{} {
124 return acme.Directory{
125 NewNonceURL: createURL(r, newNoncePath),
126 NewAccountURL: createURL(r, newAccountPath),
127 NewOrderURL: createURL(r, newOrderPath),
128 RevokeCertURL: createURL(r, revokeCertPath),
129 KeyChangeURL: createURL(r, keyChangePath),
130 }
131}
132
133func nonceHandler(w http.ResponseWriter, r *http.Request) {
134 // Hardcoded value copied from RFC 8555
135 w.Header().Add("Replay-Nonce", "oFvnlFP1wIhRlYS2jTaXbA")
136
137 w.Header().Add("Cache-Control", "no-store")
138 w.WriteHeader(http.StatusOK)
139}
140
141func accountHandler(w http.ResponseWriter, r *http.Request) interface{} {
142 return acme.Account{
143 Status: acme.StatusValid,
144 Orders: createURL(r, "orders"),
145 }
146}
147
148func newOrderHandler(w http.ResponseWriter, r *http.Request) interface{} {
149 var order acme.Order
150 err := json.NewDecoder(r.Body).Decode(&order)
151 if err != nil {
152 http.Error(w, "Bad Request", http.StatusBadRequest)
153 return nil
154 }
155
156 ordersMtx.Lock()
157 orderId := strconv.Itoa(len(orders))
158 orders = append(orders, &orderCtx{&order, nil})
159 ordersMtx.Unlock()
160
161 order.Finalize = createURL(r, path.Join(finalizePath, orderId))
162 order.Authorizations = []string{}
163
164 orderURL := createURL(r, path.Join(orderPath, orderId))
165 w.Header().Add("Location", orderURL)
166
167 w.WriteHeader(http.StatusCreated)
168 return order
169}
170
171func finalizeHandler(w http.ResponseWriter, r *http.Request) interface{} {
172 id := path.Base(r.URL.Path)
173 order, err := getOrder(r)
174 if err != nil {
175 http.Error(w, "Not Found", http.StatusNotFound)
176 return nil
177 }
178
179 var csrMsg acme.CSRMessage
180 err = json.NewDecoder(r.Body).Decode(&csrMsg)
181 if err != nil {
182 http.Error(w, "Invalid JSON", http.StatusBadRequest)
183 return nil
184 }
185
186 order.crt, err = createCrt(&csrMsg)
187 if err != nil {
188 http.Error(w, "createCrt failed", http.StatusInternalServerError)
189 return nil
190 }
191
192 order.obj.Status = acme.StatusValid
193 order.obj.Certificate = createURL(r, path.Join(certificatePath, id))
194
195 orderURL := createURL(r, path.Join(orderPath, id))
196 w.Header().Add("Location", orderURL)
197
198 return order.obj
199}
200
201func orderHandler(w http.ResponseWriter, r *http.Request) interface{} {
202 order, err := getOrder(r)
203 if err != nil {
204 http.Error(w, "Not Found", http.StatusNotFound)
205 return nil
206 }
207
208 return order.obj
209}
210
211func certHandler(w http.ResponseWriter, r *http.Request) {
212 order, err := getOrder(r)
213 if err != nil {
214 http.Error(w, "Not Found", http.StatusNotFound)
215 return
216 }
217
218 err = pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: order.crt})
219 if err != nil {
220 http.Error(w, "PEM encoding failed", http.StatusInternalServerError)
221 return
222 }
223}
224
225////
226// Middleware
227////
228
229func jsonMiddleware(fn acmeFn) http.Handler {
230 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
231 w.Header().Add("Content-Type", "application/json")
232
233 val := fn(w, r)
234 if val == nil {
235 return
236 }
237
238 err := json.NewEncoder(w).Encode(val)
239 if err != nil {
240 http.Error(w, "JSON encoding failed", http.StatusInternalServerError)
241 return
242 }
243 })
244}
245
246func jwtMiddleware(h http.Handler) http.Handler {
247 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
248 var jws jwsobj
249 err := json.NewDecoder(r.Body).Decode(&jws)
250 if err != nil {
251 http.Error(w, "Invalid JSON", http.StatusBadRequest)
252 return
253 }
254
255 payload, err := base64.RawURLEncoding.DecodeString(jws.Payload)
256 if err != nil {
257 http.Error(w, "Invalid Base64", http.StatusBadRequest)
258 return
259 }
260
261 r.Body = ioutil.NopCloser(bytes.NewReader(payload))
262 h.ServeHTTP(w, r)
263 })
264}
265
266////
267// main
268////
269
270func main() {
271 flag.Parse()
272 if *tlsKey == "" || *tlsCert == "" {
273 fmt.Fprintf(flag.CommandLine.Output(), "missing TLS key or certificate\n")
274 flag.Usage()
275 os.Exit(2)
276 }
277
278 var err error
279 key, err = rsa.GenerateKey(rand.Reader, *rsaBits)
280 if err != nil {
281 log.Fatal(err)
282 }
283
284 http.Handle(directoryPath, jsonMiddleware(directoryHandler))
285 http.HandleFunc(newNoncePath, nonceHandler)
286 http.Handle(newAccountPath, jsonMiddleware(accountHandler))
287 http.Handle(newOrderPath, jwtMiddleware(jsonMiddleware(newOrderHandler)))
288 http.Handle(finalizePath, jwtMiddleware(jsonMiddleware(finalizeHandler)))
289 http.HandleFunc(certificatePath, certHandler)
290 http.Handle(orderPath, jsonMiddleware(orderHandler))
291 log.Fatal(http.ListenAndServeTLS(*httpsAddr, *tlsCert, *tlsKey, nil))
292}