diff --git a/main.go b/main.go index 0ad9e55..b5d063b 100644 --- a/main.go +++ b/main.go @@ -399,6 +399,18 @@ func newServer(ca *ca, db *mysqlStore, allowed string, lg *slog.Logger) *server return &server{ca: ca, db: db, mgr: mgr, log: lg} } +// ----------------------------------------------------------------------------- +// helper: write JSON + (optional) fresh nonce header +// ----------------------------------------------------------------------------- +func (s *server) jsonResponse(w http.ResponseWriter, status int, v any, nonce string) { + if nonce != "" { + w.Header().Set("Replay-Nonce", nonce) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(v) +} + // ----------------------------------------------------------------------------- // JWS verification helper // ----------------------------------------------------------------------------- @@ -468,7 +480,7 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := r.Context() switch { - case r.Method == http.MethodHead && r.URL.Path == "/acme/new-nonce": + case (r.Method == http.MethodHead || r.Method == http.MethodGet) && r.URL.Path == "/acme/new-nonce": n := uuid.New().String() if err := s.db.putNonce(ctx, n); err != nil { http.Error(w, "db", 500) @@ -486,9 +498,18 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { case r.Method == http.MethodPost && strings.HasPrefix(r.URL.Path, "/acme/finalize/"): s.handleFinalize(ctx, w, r) - case r.Method == http.MethodPost && strings.HasPrefix(r.URL.Path, "/acme/revoke/"): + case r.Method == http.MethodPost && r.URL.Path == "/acme/revoke-cert": s.handleRevoke(ctx, w, r) + case r.Method == http.MethodGet && r.URL.Path == "/acme/directory": + s.handleDirectory(w, r) + + case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/acme/order/"): + s.handleGetOrder(ctx, w, r) + + case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/acme/cert/"): + s.handleGetCert(ctx, w, r) + case ocspEnabled && strings.HasPrefix(r.URL.Path, "/ocsp/"): s.handleOCSP(ctx, w, r) @@ -496,21 +517,157 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.handleCRL(ctx, w) default: - // Delegate everything else to autocert http‑01 challenge handler - lg := slog.New(slog.NewJSONHandler(os.Stdout, nil)) - slog.SetDefault(lg) - lg.Info("Call", r.URL.Path) s.mgr.HTTPHandler(nil).ServeHTTP(w, r) } } -func (s *server) jsonResponse(w http.ResponseWriter, status int, v any, nonce string) { - if nonce != "" { - w.Header().Set("Replay-Nonce", nonce) +func (s *server) handleFinalize(ctx context.Context, w http.ResponseWriter, r *http.Request) { + pay, ok := s.verifyJWS(ctx, w, r) + if !ok { + return } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - _ = json.NewEncoder(w).Encode(v) + + orderID := path.Base(r.URL.Path) + finURL := fmt.Sprintf("https://%s/acme/finalize/%s", r.Host, orderID) + + // --- decode wrapped CSR --------------------------------------------------- + var in struct { + CSR string `json:"csr"` + } + if err := json.Unmarshal(pay.Data, &in); err != nil { + http.Error(w, "bad csr wrapper", http.StatusBadRequest) + return + } + csrDER, err := base64.RawURLEncoding.DecodeString(in.CSR) + if err != nil { + http.Error(w, "bad csr b64", http.StatusBadRequest) + return + } + csr, err := x509.ParseCertificateRequest(csrDER) + if err != nil || csr.CheckSignature() != nil { + http.Error(w, "csr parse/sig", http.StatusBadRequest) + return + } + + // --- sign leaf ------------------------------------------------------------ + hash := sha256.Sum256([]byte(uuid.New().String())) + serial := new(big.Int).SetBytes(hash[:]) + + tmpl := &x509.Certificate{ + SerialNumber: serial, + Subject: csr.Subject, + NotBefore: time.Now().Add(-5 * time.Minute), + NotAfter: time.Now().Add(90 * 24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + DNSNames: csr.DNSNames, + IPAddresses: csr.IPAddresses, + PublicKey: csr.PublicKey, + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, s.ca.cert, csr.PublicKey, s.ca.key) + if err != nil { + s.log.Error("leaf sign", "err", err) + http.Error(w, "sign", http.StatusInternalServerError) + return + } + + // --- persist & reply ------------------------------------------------------ + certID := uuid.New().String() + certURL := fmt.Sprintf("https://%s/acme/cert/%s", r.Host, certID) + if err := s.db.insertCert(ctx, certID, der, serial); err != nil { + http.Error(w, "db", http.StatusInternalServerError) + return + } + if _, err := s.db.db.ExecContext(ctx, + `UPDATE orders SET status='valid', cert_url=?, payload=? WHERE id=?`, + certURL, pay.Data, orderID); err != nil { + http.Error(w, "db", http.StatusInternalServerError) + return + } + + orderURL := fmt.Sprintf("https://%s/acme/order/%s", r.Host, orderID) + nonce := uuid.New().String() + _ = s.db.putNonce(ctx, nonce) + + resp := struct { + *acme.Order + URL string `json:"url"` + }{ + Order: &acme.Order{ + Status: acme.StatusValid, + FinalizeURL: finURL, + CertURL: certURL, + }, + URL: orderURL, + } + + w.Header().Set("Location", orderURL) + s.jsonResponse(w, http.StatusOK, resp, nonce) +} + +// ----------------------------------------------------------------------------- +// GET /acme/order/{id} – order polling (Certbot/WACS) +// ----------------------------------------------------------------------------- +func (s *server) handleGetOrder(ctx context.Context, w http.ResponseWriter, r *http.Request) { + id := path.Base(r.URL.Path) + + var raw []byte + var status, finalize string + var certURL sql.NullString + if err := s.db.db.QueryRowContext(ctx, + `SELECT payload,status,finalize_url,cert_url FROM orders WHERE id=?`, id). + Scan(&raw, &status, &finalize, &certURL); err != nil { + http.Error(w, "order", http.StatusNotFound) + return + } + + var orig acme.Order + _ = json.Unmarshal(raw, &orig) + + resp := &acme.Order{ + Status: status, + Identifiers: orig.Identifiers, + FinalizeURL: finalize, + } + if certURL.Valid { + resp.CertURL = certURL.String + } + + s.jsonResponse(w, http.StatusOK, resp, "") +} + +// ----------------------------------------------------------------------------- +// GET /acme/cert/{id} – retrieve PEM chain +// ----------------------------------------------------------------------------- +func (s *server) handleGetCert(ctx context.Context, w http.ResponseWriter, r *http.Request) { + id := path.Base(r.URL.Path) + + var der []byte + if err := s.db.db.QueryRowContext(ctx, `SELECT der FROM certs WHERE id=?`, id). + Scan(&der); err != nil { + http.Error(w, "cert", http.StatusNotFound) + return + } + + var buf bytes.Buffer + _ = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: der}) + _ = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: s.ca.cert.Raw}) + + w.Header().Set("Content-Type", "application/pem-certificate-chain") + _, _ = w.Write(buf.Bytes()) +} + +// ----------------------------------------------------------------------------- +// GET /.well-known/acme-directory – capabilities +// ----------------------------------------------------------------------------- +func (s *server) handleDirectory(w http.ResponseWriter, r *http.Request) { + base := "https://" + r.Host + s.jsonResponse(w, http.StatusOK, map[string]string{ + "newNonce": base + "/acme/new-nonce", + "newAccount": base + "/acme/new-account", + "newOrder": base + "/acme/new-order", + "revokeCert": base + "/acme/revoke-cert", + }, "") } func (s *server) handleNewAccount(ctx context.Context, w http.ResponseWriter, r *http.Request) { @@ -581,7 +738,7 @@ func (s *server) handleNewOrder(ctx context.Context, w http.ResponseWriter, r *h s.jsonResponse(w, 201, resp, nonce) } -func (s *server) handleFinalize(ctx context.Context, w http.ResponseWriter, r *http.Request) { +/*func (s *server) handleFinalize(ctx context.Context, w http.ResponseWriter, r *http.Request) { pay, ok := s.verifyJWS(ctx, w, r) if !ok { return @@ -659,7 +816,7 @@ func (s *server) handleFinalize(ctx context.Context, w http.ResponseWriter, r *h w.Header().Set("Replay-Nonce", nonce) w.WriteHeader(201) _, _ = w.Write(buf.Bytes()) -} +}*/ func (s *server) handleRevoke(ctx context.Context, w http.ResponseWriter, r *http.Request) { pay, ok := s.verifyJWS(ctx, w, r)