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}
|
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
|
// JWS verification helper
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -468,7 +480,7 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
switch {
|
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()
|
n := uuid.New().String()
|
||||||
if err := s.db.putNonce(ctx, n); err != nil {
|
if err := s.db.putNonce(ctx, n); err != nil {
|
||||||
http.Error(w, "db", 500)
|
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/"):
|
case r.Method == http.MethodPost && strings.HasPrefix(r.URL.Path, "/acme/finalize/"):
|
||||||
s.handleFinalize(ctx, w, r)
|
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)
|
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/"):
|
case ocspEnabled && strings.HasPrefix(r.URL.Path, "/ocsp/"):
|
||||||
s.handleOCSP(ctx, w, r)
|
s.handleOCSP(ctx, w, r)
|
||||||
|
|
||||||
@@ -496,21 +517,157 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.handleCRL(ctx, w)
|
s.handleCRL(ctx, w)
|
||||||
|
|
||||||
default:
|
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)
|
s.mgr.HTTPHandler(nil).ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) jsonResponse(w http.ResponseWriter, status int, v any, nonce string) {
|
func (s *server) handleFinalize(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||||
if nonce != "" {
|
pay, ok := s.verifyJWS(ctx, w, r)
|
||||||
w.Header().Set("Replay-Nonce", nonce)
|
if !ok {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(status)
|
orderID := path.Base(r.URL.Path)
|
||||||
_ = json.NewEncoder(w).Encode(v)
|
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) {
|
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)
|
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)
|
pay, ok := s.verifyJWS(ctx, w, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
@@ -659,7 +816,7 @@ func (s *server) handleFinalize(ctx context.Context, w http.ResponseWriter, r *h
|
|||||||
w.Header().Set("Replay-Nonce", nonce)
|
w.Header().Set("Replay-Nonce", nonce)
|
||||||
w.WriteHeader(201)
|
w.WriteHeader(201)
|
||||||
_, _ = w.Write(buf.Bytes())
|
_, _ = w.Write(buf.Bytes())
|
||||||
}
|
}*/
|
||||||
|
|
||||||
func (s *server) handleRevoke(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
func (s *server) handleRevoke(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||||
pay, ok := s.verifyJWS(ctx, w, r)
|
pay, ok := s.verifyJWS(ctx, w, r)
|
||||||
|
Reference in New Issue
Block a user