Skip to content
File

Blob: acme/handler.go

go377 lines
1package acme
2 
3import (
4 "crypto/ecdsa"
5 "crypto/ed25519"
6 "crypto/rsa"
7 "crypto/x509"
8 "crypto/x509/pkix"
9 "encoding/json"
10 "encoding/pem"
11 "fmt"
12 "io/fs"
13 "net/http"
14 "path"
15 "strings"
16 "time"
17 
18 "github.com/caddyserver/certmagic"
19 "github.com/go-chi/chi/v5"
20)
21 
22const certsPrefix = "certificates"
23 
24type issuerInfo struct {
25 Issuer string `json:"issuer"`
26 Count int `json:"count"`
27}
28 
29type issuersResponse struct {
30 Issuers []issuerInfo `json:"issuers"`
31}
32 
33type certSummary struct {
34 Name string `json:"name"` // safe key name (e.g., wildcard_.example.com)
35 Domain string `json:"domain"` // original domain (e.g., *.example.com)
36}
37 
38type certsListResponse struct {
39 Issuer string `json:"issuer"`
40 Count int `json:"count"`
41 Certs []certSummary `json:"certs"`
42}
43 
44type publicKeyInfo struct {
45 Algorithm string `json:"algorithm"`
46 Size int `json:"size,omitempty"`
47}
48 
49type subjectInfo struct {
50 CommonName string `json:"common_name,omitempty"`
51 Organization []string `json:"organization,omitempty"`
52 OrganizationalUnit []string `json:"organizational_unit,omitempty"`
53 Country []string `json:"country,omitempty"`
54 Province []string `json:"province,omitempty"`
55 Locality []string `json:"locality,omitempty"`
56}
57 
58type certInspectResponse struct {
59 IssuerKey string `json:"issuer_key"`
60 Name string `json:"name"`
61 Domain string `json:"domain"`
62 
63 Version int `json:"version"`
64 SerialNumber string `json:"serial_number"`
65 SignatureAlgorithm string `json:"signature_algorithm"`
66 Subject subjectInfo `json:"subject"`
67 Issuer subjectInfo `json:"issuer"`
68 NotBefore time.Time `json:"not_before"`
69 NotAfter time.Time `json:"not_after"`
70 PublicKey publicKeyInfo `json:"public_key"`
71 
72 DNSNames []string `json:"dns_names,omitempty"`
73 EmailAddresses []string `json:"email_addresses,omitempty"`
74 IPAddresses []string `json:"ip_addresses,omitempty"`
75 URIs []string `json:"uris,omitempty"`
76 KeyUsage []string `json:"key_usage,omitempty"`
77 ExtKeyUsage []string `json:"ext_key_usage,omitempty"`
78 IsCA bool `json:"is_ca"`
79 
80 Metadata json.RawMessage `json:"metadata,omitempty"`
81}
82 
83func AcmeManagerHandler(m *Manager) http.Handler {
84 router := chi.NewRouter()
85 
86 router.Post("/clean", func(w http.ResponseWriter, r *http.Request) {
87 certmagic.CleanStorage(r.Context(), m.chordStorage, certmagic.CleanStorageOptions{
88 OCSPStaples: true,
89 ExpiredCerts: true,
90 ExpiredCertGracePeriod: time.Hour * 24 * 30,
91 })
92 w.WriteHeader(http.StatusNoContent)
93 })
94 
95 // GET /certs - list issuers and certificate counts
96 router.Get("/certs", func(w http.ResponseWriter, r *http.Request) {
97 ctx := r.Context()
98 keys, err := m.chordStorage.List(ctx, certsPrefix, false)
99 if err != nil {
100 http.Error(w, err.Error(), http.StatusInternalServerError)
101 return
102 }
103 
104 issuers := make([]issuerInfo, 0, len(keys))
105 for _, key := range keys {
106 issuer := strings.TrimPrefix(key, certsPrefix+"/")
107 if issuer == "" {
108 continue
109 }
110 // Count certs under this issuer
111 issuerPrefix := path.Join(certsPrefix, issuer)
112 certKeys, err := m.chordStorage.List(ctx, issuerPrefix, false)
113 if err != nil {
114 continue
115 }
116 issuers = append(issuers, issuerInfo{
117 Issuer: issuer,
118 Count: len(certKeys),
119 })
120 }
121 
122 w.Header().Set("Content-Type", "application/json")
123 json.NewEncoder(w).Encode(issuersResponse{Issuers: issuers})
124 })
125 
126 // GET /certs/{issuer} - list certificates for an issuer
127 router.Get("/certs/{issuer}", func(w http.ResponseWriter, r *http.Request) {
128 ctx := r.Context()
129 issuer := chi.URLParam(r, "issuer")
130 
131 issuerPrefix := path.Join(certsPrefix, issuer)
132 keys, err := m.chordStorage.List(ctx, issuerPrefix, false)
133 if err != nil {
134 http.Error(w, err.Error(), http.StatusInternalServerError)
135 return
136 }
137 
138 certs := make([]certSummary, 0, len(keys))
139 for _, key := range keys {
140 safeName := strings.TrimPrefix(key, issuerPrefix+"/")
141 if safeName == "" {
142 continue
143 }
144 certs = append(certs, certSummary{
145 Name: safeName,
146 Domain: unsafeName(safeName),
147 })
148 }
149 
150 w.Header().Set("Content-Type", "application/json")
151 json.NewEncoder(w).Encode(certsListResponse{
152 Issuer: issuer,
153 Count: len(certs),
154 Certs: certs,
155 })
156 })
157 
158 // GET /certs/{issuer}/{name} - inspect a certificate
159 router.Get("/certs/{issuer}/{name}", func(w http.ResponseWriter, r *http.Request) {
160 ctx := r.Context()
161 issuer := chi.URLParam(r, "issuer")
162 name := chi.URLParam(r, "name")
163 safeName := certmagic.StorageKeys.Safe(name)
164 
165 // Load certificate
166 certKey := certmagic.StorageKeys.SiteCert(issuer, name)
167 certPEM, err := m.chordStorage.Load(ctx, certKey)
168 if err != nil {
169 if err == fs.ErrNotExist {
170 http.Error(w, "certificate not found", http.StatusNotFound)
171 return
172 }
173 http.Error(w, err.Error(), http.StatusInternalServerError)
174 return
175 }
176 
177 // Parse certificate
178 block, _ := pem.Decode(certPEM)
179 if block == nil {
180 http.Error(w, "failed to decode certificate PEM", http.StatusInternalServerError)
181 return
182 }
183 
184 cert, err := x509.ParseCertificate(block.Bytes)
185 if err != nil {
186 http.Error(w, "failed to parse certificate: "+err.Error(), http.StatusInternalServerError)
187 return
188 }
189 
190 resp := certInspectResponse{
191 IssuerKey: issuer,
192 Name: safeName,
193 Domain: unsafeName(safeName),
194 Version: cert.Version,
195 SerialNumber: cert.SerialNumber.String(),
196 SignatureAlgorithm: cert.SignatureAlgorithm.String(),
197 Subject: extractSubjectInfo(cert.Subject),
198 Issuer: extractSubjectInfo(cert.Issuer),
199 NotBefore: cert.NotBefore,
200 NotAfter: cert.NotAfter,
201 PublicKey: extractPublicKeyInfo(cert),
202 DNSNames: cert.DNSNames,
203 EmailAddresses: cert.EmailAddresses,
204 IPAddresses: formatIPAddresses(cert),
205 URIs: formatURIs(cert),
206 KeyUsage: formatKeyUsage(cert.KeyUsage),
207 ExtKeyUsage: formatExtKeyUsage(cert.ExtKeyUsage),
208 IsCA: cert.IsCA,
209 }
210 
211 // Load metadata if available
212 metaKey := certmagic.StorageKeys.SiteMeta(issuer, name)
213 metaData, err := m.chordStorage.Load(ctx, metaKey)
214 if err == nil && len(metaData) > 0 {
215 resp.Metadata = metaData
216 }
217 
218 w.Header().Set("Content-Type", "application/json")
219 json.NewEncoder(w).Encode(resp)
220 })
221 
222 // DELETE /certs/{issuer}/{name} - delete a certificate
223 router.Delete("/certs/{issuer}/{name}", func(w http.ResponseWriter, r *http.Request) {
224 ctx := r.Context()
225 issuer := chi.URLParam(r, "issuer")
226 name := chi.URLParam(r, "name")
227 
228 // Delete cert, key, and meta files
229 certKey := certmagic.StorageKeys.SiteCert(issuer, name)
230 keyKey := certmagic.StorageKeys.SitePrivateKey(issuer, name)
231 metaKey := certmagic.StorageKeys.SiteMeta(issuer, name)
232 
233 var errs []string
234 if err := m.chordStorage.Delete(ctx, certKey); err != nil && err != fs.ErrNotExist {
235 errs = append(errs, "cert: "+err.Error())
236 }
237 if err := m.chordStorage.Delete(ctx, keyKey); err != nil && err != fs.ErrNotExist {
238 errs = append(errs, "key: "+err.Error())
239 }
240 if err := m.chordStorage.Delete(ctx, metaKey); err != nil && err != fs.ErrNotExist {
241 errs = append(errs, "meta: "+err.Error())
242 }
243 
244 if len(errs) > 0 {
245 http.Error(w, strings.Join(errs, "; "), http.StatusInternalServerError)
246 return
247 }
248 
249 w.WriteHeader(http.StatusNoContent)
250 })
251 
252 return router
253}
254 
255// unsafeName converts a safe storage key name back to the original domain
256// e.g., "wildcard_.example.com" -> "*.example.com"
257func unsafeName(name string) string {
258 if after, ok := strings.CutPrefix(name, "wildcard_"); ok {
259 return "*" + after
260 }
261 return name
262}
263 
264func extractSubjectInfo(name pkix.Name) subjectInfo {
265 return subjectInfo{
266 CommonName: name.CommonName,
267 Organization: name.Organization,
268 OrganizationalUnit: name.OrganizationalUnit,
269 Country: name.Country,
270 Province: name.Province,
271 Locality: name.Locality,
272 }
273}
274 
275func extractPublicKeyInfo(cert *x509.Certificate) publicKeyInfo {
276 info := publicKeyInfo{}
277 switch pub := cert.PublicKey.(type) {
278 case *rsa.PublicKey:
279 info.Algorithm = "RSA"
280 info.Size = pub.N.BitLen()
281 case *ecdsa.PublicKey:
282 info.Algorithm = fmt.Sprintf("ECDSA (%s)", pub.Curve.Params().Name)
283 info.Size = pub.Curve.Params().BitSize
284 case ed25519.PublicKey:
285 info.Algorithm = "Ed25519"
286 info.Size = 256
287 default:
288 info.Algorithm = "Unknown"
289 }
290 return info
291}
292 
293func formatIPAddresses(cert *x509.Certificate) []string {
294 if len(cert.IPAddresses) == 0 {
295 return nil
296 }
297 ips := make([]string, len(cert.IPAddresses))
298 for i, ip := range cert.IPAddresses {
299 ips[i] = ip.String()
300 }
301 return ips
302}
303 
304func formatURIs(cert *x509.Certificate) []string {
305 if len(cert.URIs) == 0 {
306 return nil
307 }
308 uris := make([]string, len(cert.URIs))
309 for i, u := range cert.URIs {
310 uris[i] = u.String()
311 }
312 return uris
313}
314 
315func formatKeyUsage(ku x509.KeyUsage) []string {
316 if ku == 0 {
317 return nil
318 }
319 var usages []string
320 if ku&x509.KeyUsageDigitalSignature != 0 {
321 usages = append(usages, "Digital Signature")
322 }
323 if ku&x509.KeyUsageContentCommitment != 0 {
324 usages = append(usages, "Content Commitment")
325 }
326 if ku&x509.KeyUsageKeyEncipherment != 0 {
327 usages = append(usages, "Key Encipherment")
328 }
329 if ku&x509.KeyUsageDataEncipherment != 0 {
330 usages = append(usages, "Data Encipherment")
331 }
332 if ku&x509.KeyUsageKeyAgreement != 0 {
333 usages = append(usages, "Key Agreement")
334 }
335 if ku&x509.KeyUsageCertSign != 0 {
336 usages = append(usages, "Certificate Sign")
337 }
338 if ku&x509.KeyUsageCRLSign != 0 {
339 usages = append(usages, "CRL Sign")
340 }
341 if ku&x509.KeyUsageEncipherOnly != 0 {
342 usages = append(usages, "Encipher Only")
343 }
344 if ku&x509.KeyUsageDecipherOnly != 0 {
345 usages = append(usages, "Decipher Only")
346 }
347 return usages
348}
349 
350func formatExtKeyUsage(ekus []x509.ExtKeyUsage) []string {
351 if len(ekus) == 0 {
352 return nil
353 }
354 var usages []string
355 for _, eku := range ekus {
356 switch eku {
357 case x509.ExtKeyUsageAny:
358 usages = append(usages, "Any")
359 case x509.ExtKeyUsageServerAuth:
360 usages = append(usages, "TLS Web Server Authentication")
361 case x509.ExtKeyUsageClientAuth:
362 usages = append(usages, "TLS Web Client Authentication")
363 case x509.ExtKeyUsageCodeSigning:
364 usages = append(usages, "Code Signing")
365 case x509.ExtKeyUsageEmailProtection:
366 usages = append(usages, "Email Protection")
367 case x509.ExtKeyUsageTimeStamping:
368 usages = append(usages, "Time Stamping")
369 case x509.ExtKeyUsageOCSPSigning:
370 usages = append(usages, "OCSP Signing")
371 default:
372 usages = append(usages, fmt.Sprintf("Unknown (%d)", eku))
373 }
374 }
375 return usages
376}