1package main23import (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)2526////27// Types28////2930type acmeFn func(http.ResponseWriter, *http.Request) interface{}3132type orderCtx struct {33 obj *acme.Order34 crt []byte35}3637type jwsobj struct {38 Protected string `json:"protected"`39 Payload string `json:"payload"`40 Signature string `json:"signature"`41}4243type authzResponse struct {44 Status string `json:"status"`45 Expires string `json:"expires"`46 Identifier Identifier `json:"identifier"`47 Challenges []Challenge `json:"challenges"`48}4950type Identifier struct {51 Type string `json:"type"`52 Value string `json:"value"`53}5455type 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}6263////64// Variables & Constants65////6667const (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"7576 finalizePath = "/finalize/"77 certificatePath = "/certificate/"78 orderPath = "/order/"7980 replayNonce = "oFvnlFP1wIhRlYS2jTaXbA"81)8283var (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)8990var key *rsa.PrivateKey91var caKey *rsa.PrivateKey92var orders []*orderCtx93var ordersMtx sync.Mutex9495////96// Utility functions97////9899func createCrt(csrMsg *acme.CSRMessage) ([]byte, error) {100 data, err := base64.RawURLEncoding.DecodeString(csrMsg.Csr)101 if err != nil {102 return nil, err103 }104105 csr, err := x509.ParseCertificateRequest(data)106 if err != nil {107 return nil, err108 }109110 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 day117 KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,118 BasicConstraintsValid: true,119 IsCA: true,120 }121122 caCert, _ := x509.CreateCertificate(rand.Reader, &caTemp, &caTemp, &caKey.PublicKey, key)123124 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 day132 }133134 crt, err := x509.CreateCertificate(rand.Reader, &certTemp, &caTemp, &key.PublicKey, caKey)135136 // Encode the certificates to PEM format137 pemCert1 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert})138 pemCert2 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: crt})139140 // Concatenate the PEM-encoded certificates141 fullChain := append(pemCert1, pemCert2...)142143 return fullChain, err144}145146func getOrder(r *http.Request) (*orderCtx, error) {147 id, err := strconv.Atoi(path.Base(r.URL.Path))148 if err != nil {149 return nil, err150 }151152 ordersMtx.Lock()153 defer ordersMtx.Unlock()154155 if id < len(orders) {156 return orders[id], nil157 } else {158 return nil, fmt.Errorf("order not found")159 }160}161162func createURL(r *http.Request, path string) string {163 r.URL.Host = r.Host164 r.URL.Scheme = "https"165 r.URL.Path = path166167 return r.URL.String()168}169170////171// Handlers172////173174func directoryHandler(w http.ResponseWriter, r *http.Request) interface{} {175176 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}185186func nonceHandler(w http.ResponseWriter, r *http.Request) {187 // Hardcoded value copied from RFC 8555188 w.Header().Add("Replay-Nonce", replayNonce)189190 w.Header().Add("Cache-Control", "no-store")191 w.WriteHeader(http.StatusOK)192}193194func accountHandler(w http.ResponseWriter, r *http.Request) interface{} {195 return acme.Account{196 Status: acme.StatusValid,197 Orders: createURL(r, "orders"),198 }199}200201func newOrderHandler(w http.ResponseWriter, r *http.Request) interface{} {202 var order acme.Order203 err := json.NewDecoder(r.Body).Decode(&order)204 if err != nil {205 http.Error(w, "Bad Request", http.StatusBadRequest)206 return nil207 }208209 ordersMtx.Lock()210 orderId := strconv.Itoa(len(orders))211 orders = append(orders, &orderCtx{&order, nil})212 ordersMtx.Unlock()213214 order.Finalize = createURL(r, path.Join(finalizePath, orderId))215216 mockChallengeURL := "https://localhost:8443/authz"217 order.Authorizations = []string{mockChallengeURL}218219 orderURL := createURL(r, path.Join(orderPath, orderId))220 w.Header().Add("Location", orderURL)221 w.Header().Add("Replay-Nonce", replayNonce)222223 w.WriteHeader(http.StatusCreated)224 return order225}226227func 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 nil233 }234235 var csrMsg acme.CSRMessage236 err = json.NewDecoder(r.Body).Decode(&csrMsg)237 if err != nil {238 http.Error(w, "Invalid JSON", http.StatusBadRequest)239 return nil240 }241242 order.crt, err = createCrt(&csrMsg)243 if err != nil {244 http.Error(w, "createCrt failed", http.StatusInternalServerError)245 return nil246 }247248 order.obj.Status = acme.StatusValid249 order.obj.Certificate = createURL(r, path.Join(certificatePath, id))250251 orderURL := createURL(r, path.Join(orderPath, id))252 w.Header().Add("Location", orderURL)253 w.Header().Add("Replay-Nonce", replayNonce)254255 return order.obj256}257258func orderHandler(w http.ResponseWriter, r *http.Request) interface{} {259 order, err := getOrder(r)260 w.Header().Add("Replay-Nonce", replayNonce)261262 if err != nil {263 http.Error(w, "Not Found", http.StatusNotFound)264 return nil265 }266267 return order.obj268}269270func certHandler(w http.ResponseWriter, r *http.Request) {271 w.Header().Set("Replay-Nonce", replayNonce)272273 order, err := getOrder(r)274 if err != nil {275 http.Error(w, "Not Found", http.StatusNotFound)276 return277 }278279 w.Write(order.crt)280}281282////283// Middleware284////285286func authzHandler(w http.ResponseWriter, r *http.Request) interface{} {287 // Get the current date and time288 currentTime := time.Now().UTC()289290 // Format the current date and time as strings291 currentTimeString := currentTime.Format(time.RFC3339)292293 // Create a authzResponse object294 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 }311312 // Set the Content-Type header313 w.Header().Set("Content-Type", "application/json")314 w.Header().Set("Replay-Nonce", replayNonce)315316 // Set the status code to 201317 w.WriteHeader(http.StatusCreated)318319 return resp320}321322func jsonMiddleware(fn acmeFn) http.Handler {323 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {324 w.Header().Add("Content-Type", "application/json")325326 val := fn(w, r)327 if val == nil {328 return329 }330331 err := json.NewEncoder(w).Encode(val)332 if err != nil {333 http.Error(w, "JSON encoding failed", http.StatusInternalServerError)334 return335 }336 })337}338339func 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)345346 val := fn(w, r)347 if val == nil {348 return349 }350351 err := json.NewEncoder(w).Encode(val)352 if err != nil {353 http.Error(w, "JSON encoding failed", http.StatusInternalServerError)354 return355 }356 })357}358359func jwtMiddleware(h http.Handler) http.Handler {360 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {361 var jws jwsobj362 err := json.NewDecoder(r.Body).Decode(&jws)363 if err != nil {364 http.Error(w, "Invalid JSON", http.StatusBadRequest)365 return366 }367368 payload, err := base64.RawURLEncoding.DecodeString(jws.Payload)369 if err != nil {370 http.Error(w, "Invalid Base64", http.StatusBadRequest)371 return372 }373374 r.Body = ioutil.NopCloser(bytes.NewReader(payload))375 h.ServeHTTP(w, r)376 })377}378379////380// main381////382383func 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 }390391 var err error392 key, err = rsa.GenerateKey(rand.Reader, *rsaBits)393 caKey, err = rsa.GenerateKey(rand.Reader, *rsaBits)394 if err != nil {395 log.Fatal(err)396 }397398 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}