main.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
  1. package main
  2. import (
  3. "archive/zip"
  4. "bytes"
  5. "crypto/rand"
  6. "crypto/rsa"
  7. "crypto/tls"
  8. "crypto/x509"
  9. "crypto/x509/pkix"
  10. "encoding/base64"
  11. "encoding/pem"
  12. "errors"
  13. "fmt"
  14. "math"
  15. "math/big"
  16. "net"
  17. "net/http"
  18. "net/url"
  19. "os"
  20. "reflect"
  21. "strings"
  22. "time"
  23. "github.com/gin-gonic/gin"
  24. )
  25. type tlsGenerateRequest struct {
  26. CommonName string `json:"commonName"`
  27. Organization string `json:"organization"`
  28. OrganizationalUnit string `json:"organizationalUnit"`
  29. Locality string `json:"locality"`
  30. State string `json:"state"`
  31. Country string `json:"country"`
  32. DNSNames string `json:"dnsNames"`
  33. ValidDays int `json:"validDays"`
  34. KeySize int `json:"keySize"`
  35. }
  36. type dnsLookupRequest struct {
  37. Host string `json:"host"`
  38. }
  39. type tslCheckRequest struct {
  40. URL string `json:"url"`
  41. }
  42. type pemCheckRequest struct {
  43. PEM string `json:"pem"`
  44. }
  45. type apiError struct {
  46. Error string `json:"error"`
  47. }
  48. func main() {
  49. port := getenv("PORT", "8080")
  50. certFile := strings.TrimSpace(os.Getenv("TLS_CERT_FILE"))
  51. keyFile := strings.TrimSpace(os.Getenv("TLS_KEY_FILE"))
  52. router := newRouter()
  53. addr := ":" + port
  54. var err error
  55. if certFile != "" || keyFile != "" {
  56. if certFile == "" || keyFile == "" {
  57. panic("both TLS_CERT_FILE and TLS_KEY_FILE must be set to enable TLS")
  58. }
  59. err = router.RunTLS(addr, certFile, keyFile)
  60. } else {
  61. err = router.Run(addr)
  62. }
  63. if err != nil {
  64. panic(err)
  65. }
  66. }
  67. func newRouter() *gin.Engine {
  68. router := gin.Default()
  69. router.LoadHTMLGlob("templates/*")
  70. router.Static("/static", "./static")
  71. _ = router.SetTrustedProxies(nil)
  72. router.GET("/", func(c *gin.Context) {
  73. c.HTML(http.StatusOK, "index.html", gin.H{
  74. "title": "Network Tools",
  75. })
  76. })
  77. api := router.Group("/api")
  78. {
  79. api.POST("/tls/generate", handleTLSGenerate)
  80. api.POST("/dns/lookup", handleDNSLookup)
  81. api.POST("/tsl/check", handleTSLCheck)
  82. api.POST("/pem/check", handlePEMCheck)
  83. }
  84. return router
  85. }
  86. func handleTLSGenerate(c *gin.Context) {
  87. var req tlsGenerateRequest
  88. if err := c.ShouldBindJSON(&req); err != nil {
  89. c.JSON(http.StatusBadRequest, apiError{Error: "invalid request body"})
  90. return
  91. }
  92. commonName := strings.TrimSpace(req.CommonName)
  93. if commonName == "" {
  94. c.JSON(http.StatusBadRequest, apiError{Error: "commonName is required"})
  95. return
  96. }
  97. validDays := req.ValidDays
  98. if validDays <= 0 {
  99. validDays = 365
  100. }
  101. keySize := normalizeKeySize(req.KeySize)
  102. if keySize == 0 {
  103. c.JSON(http.StatusBadRequest, apiError{Error: "keySize must be one of 2048, 3072, or 4096"})
  104. return
  105. }
  106. dnsNames := splitCSV(req.DNSNames)
  107. if len(dnsNames) == 0 {
  108. dnsNames = []string{commonName}
  109. }
  110. subject := pkix.Name{
  111. CommonName: commonName,
  112. Organization: splitCSV(req.Organization),
  113. OrganizationalUnit: splitCSV(req.OrganizationalUnit),
  114. Locality: splitCSV(req.Locality),
  115. Province: splitCSV(req.State),
  116. Country: splitCSV(req.Country),
  117. }
  118. certPEM, keyPEM, publicKeyPEM, csrPEM, parsed, err := generateCertificate(subject, dnsNames, validDays, keySize)
  119. if err != nil {
  120. c.JSON(http.StatusInternalServerError, apiError{Error: err.Error()})
  121. return
  122. }
  123. baseName := sanitizeFilename(commonName)
  124. zipBytes, err := buildTLSArchive(baseName, certPEM, keyPEM, publicKeyPEM, csrPEM)
  125. if err != nil {
  126. c.JSON(http.StatusInternalServerError, apiError{Error: err.Error()})
  127. return
  128. }
  129. c.JSON(http.StatusOK, gin.H{
  130. "certificatePem": string(certPEM),
  131. "selfSignedCertificatePem": string(certPEM),
  132. "privateKeyPem": string(keyPEM),
  133. "publicKeyPem": string(publicKeyPEM),
  134. "csrPem": string(csrPEM),
  135. "subject": parsed.Subject.String(),
  136. "issuer": parsed.Issuer.String(),
  137. "serialNumber": parsed.SerialNumber.String(),
  138. "notBefore": parsed.NotBefore.Format(time.RFC3339),
  139. "notAfter": parsed.NotAfter.Format(time.RFC3339),
  140. "dnsNames": parsed.DNSNames,
  141. "keySize": keySize,
  142. "isSelfSigned": parsed.Subject.String() == parsed.Issuer.String(),
  143. "organization": parsed.Subject.Organization,
  144. "ou": parsed.Subject.OrganizationalUnit,
  145. "locality": parsed.Subject.Locality,
  146. "state": parsed.Subject.Province,
  147. "country": parsed.Subject.Country,
  148. "zipFilename": baseName + ".zip",
  149. "zipBase64": base64.StdEncoding.EncodeToString(zipBytes),
  150. })
  151. }
  152. func handleDNSLookup(c *gin.Context) {
  153. var req dnsLookupRequest
  154. if err := c.ShouldBindJSON(&req); err != nil {
  155. c.JSON(http.StatusBadRequest, apiError{Error: "invalid request body"})
  156. return
  157. }
  158. host := normalizeHost(req.Host)
  159. if host == "" {
  160. c.JSON(http.StatusBadRequest, apiError{Error: "host is required"})
  161. return
  162. }
  163. ips, ipErr := net.LookupIP(host)
  164. txt, txtErr := net.LookupTXT(host)
  165. mx, mxErr := net.LookupMX(host)
  166. ns, nsErr := net.LookupNS(host)
  167. cname, cnameErr := net.LookupCNAME(host)
  168. if ipErr != nil && txtErr != nil && mxErr != nil && nsErr != nil && cnameErr != nil {
  169. c.JSON(http.StatusBadGateway, apiError{Error: fmt.Sprintf("lookup failed for %s", host)})
  170. return
  171. }
  172. ipStrings := make([]string, 0, len(ips))
  173. for _, ip := range ips {
  174. ipStrings = append(ipStrings, ip.String())
  175. }
  176. txtStrings := make([]string, 0, len(txt))
  177. txtStrings = append(txtStrings, txt...)
  178. mxStrings := make([]gin.H, 0, len(mx))
  179. for _, record := range mx {
  180. mxStrings = append(mxStrings, gin.H{
  181. "host": record.Host,
  182. "pref": record.Pref,
  183. })
  184. }
  185. nsStrings := make([]string, 0, len(ns))
  186. for _, record := range ns {
  187. nsStrings = append(nsStrings, record.Host)
  188. }
  189. errorsByType := gin.H{}
  190. if ipErr != nil {
  191. errorsByType["a_aaaa"] = ipErr.Error()
  192. }
  193. if txtErr != nil {
  194. errorsByType["txt"] = txtErr.Error()
  195. }
  196. if mxErr != nil {
  197. errorsByType["mx"] = mxErr.Error()
  198. }
  199. if nsErr != nil {
  200. errorsByType["ns"] = nsErr.Error()
  201. }
  202. if cnameErr != nil {
  203. errorsByType["cname"] = cnameErr.Error()
  204. }
  205. c.JSON(http.StatusOK, gin.H{
  206. "host": host,
  207. "ips": ipStrings,
  208. "txt": txtStrings,
  209. "mx": mxStrings,
  210. "ns": nsStrings,
  211. "cname": strings.TrimSuffix(cname, "."),
  212. "errors": errorsByType,
  213. })
  214. }
  215. func handleTSLCheck(c *gin.Context) {
  216. var req tslCheckRequest
  217. if err := c.ShouldBindJSON(&req); err != nil {
  218. c.JSON(http.StatusBadRequest, apiError{Error: "invalid request body"})
  219. return
  220. }
  221. targetURL := strings.TrimSpace(req.URL)
  222. if targetURL == "" {
  223. c.JSON(http.StatusBadRequest, apiError{Error: "url is required"})
  224. return
  225. }
  226. parsedURL, err := normalizeURL(targetURL)
  227. if err != nil {
  228. c.JSON(http.StatusBadRequest, apiError{Error: err.Error()})
  229. return
  230. }
  231. host := parsedURL.Hostname()
  232. port := parsedURL.Port()
  233. if port == "" {
  234. port = "443"
  235. }
  236. conn, err := tls.DialWithDialer(
  237. &net.Dialer{Timeout: 10 * time.Second},
  238. "tcp",
  239. net.JoinHostPort(host, port),
  240. &tls.Config{ServerName: host},
  241. )
  242. if err != nil {
  243. c.JSON(http.StatusBadGateway, apiError{Error: err.Error()})
  244. return
  245. }
  246. defer conn.Close()
  247. state := conn.ConnectionState()
  248. if len(state.PeerCertificates) == 0 {
  249. c.JSON(http.StatusBadGateway, apiError{Error: "no peer certificates returned"})
  250. return
  251. }
  252. leaf := state.PeerCertificates[0]
  253. certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leaf.Raw})
  254. chain := make([]gin.H, 0, len(state.PeerCertificates))
  255. for _, cert := range state.PeerCertificates {
  256. chain = append(chain, gin.H{
  257. "subject": cert.Subject.String(),
  258. "issuer": cert.Issuer.String(),
  259. "serialNumber": cert.SerialNumber.String(),
  260. "notBefore": cert.NotBefore.Format(time.RFC3339),
  261. "notAfter": cert.NotAfter.Format(time.RFC3339),
  262. "dnsNames": cert.DNSNames,
  263. })
  264. }
  265. validDays := int(math.Ceil(leaf.NotAfter.Sub(leaf.NotBefore).Hours() / 24))
  266. if validDays < 1 {
  267. validDays = 1
  268. }
  269. keySize := publicKeySize(leaf.PublicKey)
  270. if keySize == 0 {
  271. keySize = 2048
  272. }
  273. c.JSON(http.StatusOK, gin.H{
  274. "url": parsedURL.String(),
  275. "serverName": host,
  276. "version": tlsVersionName(state.Version),
  277. "cipherSuite": tls.CipherSuiteName(state.CipherSuite),
  278. "negotiatedProtocol": state.NegotiatedProtocol,
  279. "certificatePem": string(certPEM),
  280. "chain": chain,
  281. "leafTemplate": gin.H{
  282. "commonName": leaf.Subject.CommonName,
  283. "organization": strings.Join(leaf.Subject.Organization, ", "),
  284. "organizationalUnit": strings.Join(leaf.Subject.OrganizationalUnit, ", "),
  285. "locality": strings.Join(leaf.Subject.Locality, ", "),
  286. "state": strings.Join(leaf.Subject.Province, ", "),
  287. "country": strings.Join(leaf.Subject.Country, ", "),
  288. "dnsNames": strings.Join(leaf.DNSNames, ", "),
  289. "validDays": validDays,
  290. "keySize": keySize,
  291. },
  292. })
  293. }
  294. func handlePEMCheck(c *gin.Context) {
  295. var req pemCheckRequest
  296. if err := c.ShouldBindJSON(&req); err != nil {
  297. c.JSON(http.StatusBadRequest, apiError{Error: "invalid request body"})
  298. return
  299. }
  300. input := strings.TrimSpace(req.PEM)
  301. if input == "" {
  302. c.JSON(http.StatusBadRequest, apiError{Error: "pem is required"})
  303. return
  304. }
  305. blocks, err := inspectPEM(input)
  306. if err != nil {
  307. c.JSON(http.StatusBadRequest, apiError{Error: err.Error()})
  308. return
  309. }
  310. c.JSON(http.StatusOK, gin.H{
  311. "blocks": blocks,
  312. "count": len(blocks),
  313. })
  314. }
  315. func generateCertificate(subject pkix.Name, dnsNames []string, validDays int, keySize int) ([]byte, []byte, []byte, []byte, *x509.Certificate, error) {
  316. privateKey, err := rsa.GenerateKey(rand.Reader, keySize)
  317. if err != nil {
  318. return nil, nil, nil, nil, nil, err
  319. }
  320. serialLimit := new(big.Int).Lsh(big.NewInt(1), 128)
  321. serialNumber, err := rand.Int(rand.Reader, serialLimit)
  322. if err != nil {
  323. return nil, nil, nil, nil, nil, err
  324. }
  325. notBefore := time.Now().UTC()
  326. template := &x509.Certificate{
  327. SerialNumber: serialNumber,
  328. Subject: subject,
  329. NotBefore: notBefore,
  330. NotAfter: notBefore.Add(time.Duration(validDays) * 24 * time.Hour),
  331. KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
  332. ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
  333. BasicConstraintsValid: true,
  334. DNSNames: dnsNames,
  335. }
  336. derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
  337. if err != nil {
  338. return nil, nil, nil, nil, nil, err
  339. }
  340. csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
  341. Subject: subject,
  342. SignatureAlgorithm: x509.SHA256WithRSA,
  343. DNSNames: dnsNames,
  344. }, privateKey)
  345. if err != nil {
  346. return nil, nil, nil, nil, nil, err
  347. }
  348. publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
  349. if err != nil {
  350. return nil, nil, nil, nil, nil, err
  351. }
  352. certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
  353. keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
  354. publicKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: publicKeyBytes})
  355. csrPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes})
  356. parsedCert, err := x509.ParseCertificate(derBytes)
  357. if err != nil {
  358. return nil, nil, nil, nil, nil, err
  359. }
  360. return certPEM, keyPEM, publicKeyPEM, csrPEM, parsedCert, nil
  361. }
  362. func buildTLSArchive(baseName string, certPEM []byte, keyPEM []byte, publicKeyPEM []byte, csrPEM []byte) ([]byte, error) {
  363. var buffer bytes.Buffer
  364. archive := zip.NewWriter(&buffer)
  365. files := []struct {
  366. name string
  367. content []byte
  368. }{
  369. {name: baseName + ".crt.pem", content: certPEM},
  370. {name: baseName + ".key.pem", content: keyPEM},
  371. {name: baseName + ".pub.pem", content: publicKeyPEM},
  372. {name: baseName + ".csr.pem", content: csrPEM},
  373. }
  374. for _, file := range files {
  375. writer, err := archive.Create(file.name)
  376. if err != nil {
  377. return nil, err
  378. }
  379. if _, err := writer.Write(file.content); err != nil {
  380. return nil, err
  381. }
  382. }
  383. if err := archive.Close(); err != nil {
  384. return nil, err
  385. }
  386. return buffer.Bytes(), nil
  387. }
  388. func inspectPEM(input string) ([]gin.H, error) {
  389. var blocks []gin.H
  390. remaining := []byte(input)
  391. for len(strings.TrimSpace(string(remaining))) > 0 {
  392. block, rest := pem.Decode(remaining)
  393. if block == nil {
  394. return nil, errors.New("unable to decode PEM data")
  395. }
  396. inspected := gin.H{
  397. "pemType": block.Type,
  398. }
  399. switch block.Type {
  400. case "CERTIFICATE":
  401. cert, err := x509.ParseCertificate(block.Bytes)
  402. if err != nil {
  403. return nil, err
  404. }
  405. inspected["kind"] = "certificate"
  406. inspected["subject"] = cert.Subject.String()
  407. inspected["issuer"] = cert.Issuer.String()
  408. inspected["serialNumber"] = cert.SerialNumber.String()
  409. inspected["notBefore"] = cert.NotBefore.Format(time.RFC3339)
  410. inspected["notAfter"] = cert.NotAfter.Format(time.RFC3339)
  411. inspected["dnsNames"] = cert.DNSNames
  412. inspected["isCA"] = cert.IsCA
  413. inspected["signatureAlgorithm"] = cert.SignatureAlgorithm.String()
  414. inspected["publicKeyAlgorithm"] = cert.PublicKeyAlgorithm.String()
  415. case "CERTIFICATE REQUEST", "NEW CERTIFICATE REQUEST":
  416. csr, err := x509.ParseCertificateRequest(block.Bytes)
  417. if err != nil {
  418. return nil, err
  419. }
  420. inspected["kind"] = "certificate_request"
  421. inspected["subject"] = csr.Subject.String()
  422. inspected["dnsNames"] = csr.DNSNames
  423. inspected["emailAddresses"] = csr.EmailAddresses
  424. inspected["signatureAlgorithm"] = csr.SignatureAlgorithm.String()
  425. inspected["publicKeyAlgorithm"] = csr.PublicKeyAlgorithm.String()
  426. default:
  427. keyInfo, err := inspectKeyBlock(block)
  428. if err != nil {
  429. return nil, err
  430. }
  431. for key, value := range keyInfo {
  432. inspected[key] = value
  433. }
  434. }
  435. blocks = append(blocks, inspected)
  436. remaining = rest
  437. }
  438. if len(blocks) == 0 {
  439. return nil, errors.New("no PEM blocks found")
  440. }
  441. return blocks, nil
  442. }
  443. func inspectKeyBlock(block *pem.Block) (gin.H, error) {
  444. switch block.Type {
  445. case "RSA PRIVATE KEY":
  446. key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
  447. if err != nil {
  448. return nil, err
  449. }
  450. return gin.H{
  451. "kind": "private_key",
  452. "algorithm": "RSA",
  453. "size": key.N.BitLen(),
  454. }, nil
  455. case "EC PRIVATE KEY":
  456. key, err := x509.ParseECPrivateKey(block.Bytes)
  457. if err != nil {
  458. return nil, err
  459. }
  460. return gin.H{
  461. "kind": "private_key",
  462. "algorithm": "EC",
  463. "curve": key.Curve.Params().Name,
  464. }, nil
  465. case "PRIVATE KEY":
  466. key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
  467. if err != nil {
  468. return nil, err
  469. }
  470. return describePrivateKey(key), nil
  471. case "PUBLIC KEY":
  472. key, err := x509.ParsePKIXPublicKey(block.Bytes)
  473. if err != nil {
  474. return nil, err
  475. }
  476. return describePublicKey(key), nil
  477. default:
  478. return nil, fmt.Errorf("unsupported PEM block type: %s", block.Type)
  479. }
  480. }
  481. func describePrivateKey(key any) gin.H {
  482. switch typed := key.(type) {
  483. case *rsa.PrivateKey:
  484. return gin.H{
  485. "kind": "private_key",
  486. "algorithm": "RSA",
  487. "size": typed.N.BitLen(),
  488. }
  489. default:
  490. return gin.H{
  491. "kind": "private_key",
  492. "algorithm": reflect.TypeOf(key).String(),
  493. }
  494. }
  495. }
  496. func describePublicKey(key any) gin.H {
  497. switch typed := key.(type) {
  498. case *rsa.PublicKey:
  499. return gin.H{
  500. "kind": "public_key",
  501. "algorithm": "RSA",
  502. "size": typed.N.BitLen(),
  503. }
  504. default:
  505. return gin.H{
  506. "kind": "public_key",
  507. "algorithm": reflect.TypeOf(key).String(),
  508. }
  509. }
  510. }
  511. func splitCSV(input string) []string {
  512. parts := strings.Split(input, ",")
  513. values := make([]string, 0, len(parts))
  514. for _, part := range parts {
  515. trimmed := strings.TrimSpace(part)
  516. if trimmed != "" {
  517. values = append(values, trimmed)
  518. }
  519. }
  520. return values
  521. }
  522. func normalizeHost(input string) string {
  523. value := strings.TrimSpace(input)
  524. value = strings.TrimPrefix(value, "https://")
  525. value = strings.TrimPrefix(value, "http://")
  526. value = strings.Trim(value, "/")
  527. if host, _, err := net.SplitHostPort(value); err == nil {
  528. return host
  529. }
  530. if parsed, err := url.Parse("https://" + value); err == nil {
  531. return parsed.Hostname()
  532. }
  533. return value
  534. }
  535. func normalizeURL(input string) (*url.URL, error) {
  536. value := strings.TrimSpace(input)
  537. if value == "" {
  538. return nil, errors.New("url is required")
  539. }
  540. if !strings.Contains(value, "://") {
  541. value = "https://" + value
  542. }
  543. parsed, err := url.Parse(value)
  544. if err != nil {
  545. return nil, err
  546. }
  547. if parsed.Hostname() == "" {
  548. return nil, errors.New("url must include a hostname")
  549. }
  550. return parsed, nil
  551. }
  552. func tlsVersionName(version uint16) string {
  553. switch version {
  554. case tls.VersionTLS10:
  555. return "TLS 1.0"
  556. case tls.VersionTLS11:
  557. return "TLS 1.1"
  558. case tls.VersionTLS12:
  559. return "TLS 1.2"
  560. case tls.VersionTLS13:
  561. return "TLS 1.3"
  562. default:
  563. return fmt.Sprintf("0x%x", version)
  564. }
  565. }
  566. func normalizeKeySize(value int) int {
  567. switch value {
  568. case 0, 2048:
  569. return 2048
  570. case 3072:
  571. return 3072
  572. case 4096:
  573. return 4096
  574. default:
  575. return 0
  576. }
  577. }
  578. func sanitizeFilename(value string) string {
  579. trimmed := strings.TrimSpace(strings.ToLower(value))
  580. if trimmed == "" {
  581. return "certificate"
  582. }
  583. var builder strings.Builder
  584. lastDash := false
  585. for _, r := range trimmed {
  586. isAlphaNum := (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9')
  587. if isAlphaNum {
  588. builder.WriteRune(r)
  589. lastDash = false
  590. continue
  591. }
  592. if !lastDash {
  593. builder.WriteRune('-')
  594. lastDash = true
  595. }
  596. }
  597. result := strings.Trim(builder.String(), "-")
  598. if result == "" {
  599. return "certificate"
  600. }
  601. return result
  602. }
  603. func publicKeySize(key any) int {
  604. switch typed := key.(type) {
  605. case *rsa.PublicKey:
  606. return typed.N.BitLen()
  607. default:
  608. return 0
  609. }
  610. }
  611. func getenv(key, fallback string) string {
  612. value := strings.TrimSpace(os.Getenv(key))
  613. if value == "" {
  614. return fallback
  615. }
  616. return value
  617. }