This commit is contained in:
185
main.go
185
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)
|
||||
|
Reference in New Issue
Block a user