Patch
All checks were successful
release-tag / release-image (push) Successful in 1m44s

This commit is contained in:
jbergner
2025-04-29 14:05:06 +02:00
parent 48289cc90d
commit 47e89b46bc

185
main.go
View File

@@ -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 http01 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)