|
|
@@ -0,0 +1,669 @@
|
|
|
+package main
|
|
|
+
|
|
|
+import (
|
|
|
+ "archive/zip"
|
|
|
+ "bytes"
|
|
|
+ "crypto/rand"
|
|
|
+ "crypto/rsa"
|
|
|
+ "crypto/tls"
|
|
|
+ "crypto/x509"
|
|
|
+ "crypto/x509/pkix"
|
|
|
+ "encoding/base64"
|
|
|
+ "encoding/pem"
|
|
|
+ "errors"
|
|
|
+ "fmt"
|
|
|
+ "math/big"
|
|
|
+ "net"
|
|
|
+ "net/http"
|
|
|
+ "net/url"
|
|
|
+ "os"
|
|
|
+ "reflect"
|
|
|
+ "strings"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ "github.com/gin-gonic/gin"
|
|
|
+)
|
|
|
+
|
|
|
+type tlsGenerateRequest struct {
|
|
|
+ CommonName string `json:"commonName"`
|
|
|
+ Organization string `json:"organization"`
|
|
|
+ OrganizationalUnit string `json:"organizationalUnit"`
|
|
|
+ Locality string `json:"locality"`
|
|
|
+ State string `json:"state"`
|
|
|
+ Country string `json:"country"`
|
|
|
+ DNSNames string `json:"dnsNames"`
|
|
|
+ ValidDays int `json:"validDays"`
|
|
|
+ KeySize int `json:"keySize"`
|
|
|
+}
|
|
|
+
|
|
|
+type dnsLookupRequest struct {
|
|
|
+ Host string `json:"host"`
|
|
|
+}
|
|
|
+
|
|
|
+type sslCheckRequest struct {
|
|
|
+ URL string `json:"url"`
|
|
|
+}
|
|
|
+
|
|
|
+type pemCheckRequest struct {
|
|
|
+ PEM string `json:"pem"`
|
|
|
+}
|
|
|
+
|
|
|
+type apiError struct {
|
|
|
+ Error string `json:"error"`
|
|
|
+}
|
|
|
+
|
|
|
+func main() {
|
|
|
+ port := getenv("PORT", "8080")
|
|
|
+ certFile := strings.TrimSpace(os.Getenv("TLS_CERT_FILE"))
|
|
|
+ keyFile := strings.TrimSpace(os.Getenv("TLS_KEY_FILE"))
|
|
|
+
|
|
|
+ router := newRouter()
|
|
|
+ addr := ":" + port
|
|
|
+
|
|
|
+ var err error
|
|
|
+ if certFile != "" || keyFile != "" {
|
|
|
+ if certFile == "" || keyFile == "" {
|
|
|
+ panic("both TLS_CERT_FILE and TLS_KEY_FILE must be set to enable TLS")
|
|
|
+ }
|
|
|
+ err = router.RunTLS(addr, certFile, keyFile)
|
|
|
+ } else {
|
|
|
+ err = router.Run(addr)
|
|
|
+ }
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ panic(err)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func newRouter() *gin.Engine {
|
|
|
+ router := gin.Default()
|
|
|
+ router.LoadHTMLGlob("templates/*")
|
|
|
+ router.Static("/static", "./static")
|
|
|
+ _ = router.SetTrustedProxies(nil)
|
|
|
+
|
|
|
+ router.GET("/", func(c *gin.Context) {
|
|
|
+ c.HTML(http.StatusOK, "index.html", gin.H{
|
|
|
+ "title": "Network Tools",
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ api := router.Group("/api")
|
|
|
+ {
|
|
|
+ api.POST("/tls/generate", handleTLSGenerate)
|
|
|
+ api.POST("/dns/lookup", handleDNSLookup)
|
|
|
+ api.POST("/ssl/check", handleSSLCheck)
|
|
|
+ api.POST("/pem/check", handlePEMCheck)
|
|
|
+ }
|
|
|
+
|
|
|
+ return router
|
|
|
+}
|
|
|
+
|
|
|
+func handleTLSGenerate(c *gin.Context) {
|
|
|
+ var req tlsGenerateRequest
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
+ c.JSON(http.StatusBadRequest, apiError{Error: "invalid request body"})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ commonName := strings.TrimSpace(req.CommonName)
|
|
|
+ if commonName == "" {
|
|
|
+ c.JSON(http.StatusBadRequest, apiError{Error: "commonName is required"})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ validDays := req.ValidDays
|
|
|
+ if validDays <= 0 {
|
|
|
+ validDays = 365
|
|
|
+ }
|
|
|
+
|
|
|
+ keySize := normalizeKeySize(req.KeySize)
|
|
|
+ if keySize == 0 {
|
|
|
+ c.JSON(http.StatusBadRequest, apiError{Error: "keySize must be one of 2048, 3072, or 4096"})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ dnsNames := splitCSV(req.DNSNames)
|
|
|
+ if len(dnsNames) == 0 {
|
|
|
+ dnsNames = []string{commonName}
|
|
|
+ }
|
|
|
+
|
|
|
+ subject := pkix.Name{
|
|
|
+ CommonName: commonName,
|
|
|
+ Organization: splitCSV(req.Organization),
|
|
|
+ OrganizationalUnit: splitCSV(req.OrganizationalUnit),
|
|
|
+ Locality: splitCSV(req.Locality),
|
|
|
+ Province: splitCSV(req.State),
|
|
|
+ Country: splitCSV(req.Country),
|
|
|
+ }
|
|
|
+
|
|
|
+ certPEM, keyPEM, publicKeyPEM, csrPEM, parsed, err := generateCertificate(subject, dnsNames, validDays, keySize)
|
|
|
+ if err != nil {
|
|
|
+ c.JSON(http.StatusInternalServerError, apiError{Error: err.Error()})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ baseName := sanitizeFilename(commonName)
|
|
|
+ zipBytes, err := buildTLSArchive(baseName, certPEM, keyPEM, publicKeyPEM, csrPEM)
|
|
|
+ if err != nil {
|
|
|
+ c.JSON(http.StatusInternalServerError, apiError{Error: err.Error()})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ c.JSON(http.StatusOK, gin.H{
|
|
|
+ "certificatePem": string(certPEM),
|
|
|
+ "selfSignedCertificatePem": string(certPEM),
|
|
|
+ "privateKeyPem": string(keyPEM),
|
|
|
+ "publicKeyPem": string(publicKeyPEM),
|
|
|
+ "csrPem": string(csrPEM),
|
|
|
+ "subject": parsed.Subject.String(),
|
|
|
+ "issuer": parsed.Issuer.String(),
|
|
|
+ "serialNumber": parsed.SerialNumber.String(),
|
|
|
+ "notBefore": parsed.NotBefore.Format(time.RFC3339),
|
|
|
+ "notAfter": parsed.NotAfter.Format(time.RFC3339),
|
|
|
+ "dnsNames": parsed.DNSNames,
|
|
|
+ "keySize": keySize,
|
|
|
+ "isSelfSigned": parsed.Subject.String() == parsed.Issuer.String(),
|
|
|
+ "organization": parsed.Subject.Organization,
|
|
|
+ "ou": parsed.Subject.OrganizationalUnit,
|
|
|
+ "locality": parsed.Subject.Locality,
|
|
|
+ "state": parsed.Subject.Province,
|
|
|
+ "country": parsed.Subject.Country,
|
|
|
+ "zipFilename": baseName + ".zip",
|
|
|
+ "zipBase64": base64.StdEncoding.EncodeToString(zipBytes),
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+func handleDNSLookup(c *gin.Context) {
|
|
|
+ var req dnsLookupRequest
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
+ c.JSON(http.StatusBadRequest, apiError{Error: "invalid request body"})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ host := normalizeHost(req.Host)
|
|
|
+ if host == "" {
|
|
|
+ c.JSON(http.StatusBadRequest, apiError{Error: "host is required"})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ ips, ipErr := net.LookupIP(host)
|
|
|
+ txt, txtErr := net.LookupTXT(host)
|
|
|
+ mx, mxErr := net.LookupMX(host)
|
|
|
+ ns, nsErr := net.LookupNS(host)
|
|
|
+ cname, cnameErr := net.LookupCNAME(host)
|
|
|
+
|
|
|
+ if ipErr != nil && txtErr != nil && mxErr != nil && nsErr != nil && cnameErr != nil {
|
|
|
+ c.JSON(http.StatusBadGateway, apiError{Error: fmt.Sprintf("lookup failed for %s", host)})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ ipStrings := make([]string, 0, len(ips))
|
|
|
+ for _, ip := range ips {
|
|
|
+ ipStrings = append(ipStrings, ip.String())
|
|
|
+ }
|
|
|
+
|
|
|
+ txtStrings := make([]string, 0, len(txt))
|
|
|
+ txtStrings = append(txtStrings, txt...)
|
|
|
+
|
|
|
+ mxStrings := make([]gin.H, 0, len(mx))
|
|
|
+ for _, record := range mx {
|
|
|
+ mxStrings = append(mxStrings, gin.H{
|
|
|
+ "host": record.Host,
|
|
|
+ "pref": record.Pref,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ nsStrings := make([]string, 0, len(ns))
|
|
|
+ for _, record := range ns {
|
|
|
+ nsStrings = append(nsStrings, record.Host)
|
|
|
+ }
|
|
|
+
|
|
|
+ errorsByType := gin.H{}
|
|
|
+ if ipErr != nil {
|
|
|
+ errorsByType["a_aaaa"] = ipErr.Error()
|
|
|
+ }
|
|
|
+ if txtErr != nil {
|
|
|
+ errorsByType["txt"] = txtErr.Error()
|
|
|
+ }
|
|
|
+ if mxErr != nil {
|
|
|
+ errorsByType["mx"] = mxErr.Error()
|
|
|
+ }
|
|
|
+ if nsErr != nil {
|
|
|
+ errorsByType["ns"] = nsErr.Error()
|
|
|
+ }
|
|
|
+ if cnameErr != nil {
|
|
|
+ errorsByType["cname"] = cnameErr.Error()
|
|
|
+ }
|
|
|
+
|
|
|
+ c.JSON(http.StatusOK, gin.H{
|
|
|
+ "host": host,
|
|
|
+ "ips": ipStrings,
|
|
|
+ "txt": txtStrings,
|
|
|
+ "mx": mxStrings,
|
|
|
+ "ns": nsStrings,
|
|
|
+ "cname": strings.TrimSuffix(cname, "."),
|
|
|
+ "errors": errorsByType,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+func handleSSLCheck(c *gin.Context) {
|
|
|
+ var req sslCheckRequest
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
+ c.JSON(http.StatusBadRequest, apiError{Error: "invalid request body"})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ targetURL := strings.TrimSpace(req.URL)
|
|
|
+ if targetURL == "" {
|
|
|
+ c.JSON(http.StatusBadRequest, apiError{Error: "url is required"})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ parsedURL, err := normalizeURL(targetURL)
|
|
|
+ if err != nil {
|
|
|
+ c.JSON(http.StatusBadRequest, apiError{Error: err.Error()})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ host := parsedURL.Hostname()
|
|
|
+ port := parsedURL.Port()
|
|
|
+ if port == "" {
|
|
|
+ port = "443"
|
|
|
+ }
|
|
|
+
|
|
|
+ conn, err := tls.DialWithDialer(
|
|
|
+ &net.Dialer{Timeout: 10 * time.Second},
|
|
|
+ "tcp",
|
|
|
+ net.JoinHostPort(host, port),
|
|
|
+ &tls.Config{ServerName: host},
|
|
|
+ )
|
|
|
+ if err != nil {
|
|
|
+ c.JSON(http.StatusBadGateway, apiError{Error: err.Error()})
|
|
|
+ return
|
|
|
+ }
|
|
|
+ defer conn.Close()
|
|
|
+
|
|
|
+ state := conn.ConnectionState()
|
|
|
+ if len(state.PeerCertificates) == 0 {
|
|
|
+ c.JSON(http.StatusBadGateway, apiError{Error: "no peer certificates returned"})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ leaf := state.PeerCertificates[0]
|
|
|
+ certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leaf.Raw})
|
|
|
+
|
|
|
+ chain := make([]gin.H, 0, len(state.PeerCertificates))
|
|
|
+ for _, cert := range state.PeerCertificates {
|
|
|
+ chain = append(chain, gin.H{
|
|
|
+ "subject": cert.Subject.String(),
|
|
|
+ "issuer": cert.Issuer.String(),
|
|
|
+ "serialNumber": cert.SerialNumber.String(),
|
|
|
+ "notBefore": cert.NotBefore.Format(time.RFC3339),
|
|
|
+ "notAfter": cert.NotAfter.Format(time.RFC3339),
|
|
|
+ "dnsNames": cert.DNSNames,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ c.JSON(http.StatusOK, gin.H{
|
|
|
+ "url": parsedURL.String(),
|
|
|
+ "serverName": host,
|
|
|
+ "version": tlsVersionName(state.Version),
|
|
|
+ "cipherSuite": tls.CipherSuiteName(state.CipherSuite),
|
|
|
+ "negotiatedProtocol": state.NegotiatedProtocol,
|
|
|
+ "certificatePem": string(certPEM),
|
|
|
+ "chain": chain,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+func handlePEMCheck(c *gin.Context) {
|
|
|
+ var req pemCheckRequest
|
|
|
+ if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
+ c.JSON(http.StatusBadRequest, apiError{Error: "invalid request body"})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ input := strings.TrimSpace(req.PEM)
|
|
|
+ if input == "" {
|
|
|
+ c.JSON(http.StatusBadRequest, apiError{Error: "pem is required"})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ blocks, err := inspectPEM(input)
|
|
|
+ if err != nil {
|
|
|
+ c.JSON(http.StatusBadRequest, apiError{Error: err.Error()})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ c.JSON(http.StatusOK, gin.H{
|
|
|
+ "blocks": blocks,
|
|
|
+ "count": len(blocks),
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+func generateCertificate(subject pkix.Name, dnsNames []string, validDays int, keySize int) ([]byte, []byte, []byte, []byte, *x509.Certificate, error) {
|
|
|
+ privateKey, err := rsa.GenerateKey(rand.Reader, keySize)
|
|
|
+ if err != nil {
|
|
|
+ return nil, nil, nil, nil, nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ serialLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
|
|
+ serialNumber, err := rand.Int(rand.Reader, serialLimit)
|
|
|
+ if err != nil {
|
|
|
+ return nil, nil, nil, nil, nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ notBefore := time.Now().UTC()
|
|
|
+ template := &x509.Certificate{
|
|
|
+ SerialNumber: serialNumber,
|
|
|
+ Subject: subject,
|
|
|
+ NotBefore: notBefore,
|
|
|
+ NotAfter: notBefore.Add(time.Duration(validDays) * 24 * time.Hour),
|
|
|
+ KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
|
|
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
|
|
+ BasicConstraintsValid: true,
|
|
|
+ DNSNames: dnsNames,
|
|
|
+ }
|
|
|
+
|
|
|
+ derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
|
|
|
+ if err != nil {
|
|
|
+ return nil, nil, nil, nil, nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
|
|
|
+ Subject: subject,
|
|
|
+ SignatureAlgorithm: x509.SHA256WithRSA,
|
|
|
+ DNSNames: dnsNames,
|
|
|
+ }, privateKey)
|
|
|
+ if err != nil {
|
|
|
+ return nil, nil, nil, nil, nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
|
|
|
+ if err != nil {
|
|
|
+ return nil, nil, nil, nil, nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
|
|
+ keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
|
|
|
+ publicKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: publicKeyBytes})
|
|
|
+ csrPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes})
|
|
|
+
|
|
|
+ parsedCert, err := x509.ParseCertificate(derBytes)
|
|
|
+ if err != nil {
|
|
|
+ return nil, nil, nil, nil, nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ return certPEM, keyPEM, publicKeyPEM, csrPEM, parsedCert, nil
|
|
|
+}
|
|
|
+
|
|
|
+func buildTLSArchive(baseName string, certPEM []byte, keyPEM []byte, publicKeyPEM []byte, csrPEM []byte) ([]byte, error) {
|
|
|
+ var buffer bytes.Buffer
|
|
|
+ archive := zip.NewWriter(&buffer)
|
|
|
+
|
|
|
+ files := []struct {
|
|
|
+ name string
|
|
|
+ content []byte
|
|
|
+ }{
|
|
|
+ {name: baseName + ".crt.pem", content: certPEM},
|
|
|
+ {name: baseName + ".key.pem", content: keyPEM},
|
|
|
+ {name: baseName + ".pub.pem", content: publicKeyPEM},
|
|
|
+ {name: baseName + ".csr.pem", content: csrPEM},
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, file := range files {
|
|
|
+ writer, err := archive.Create(file.name)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ if _, err := writer.Write(file.content); err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := archive.Close(); err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ return buffer.Bytes(), nil
|
|
|
+}
|
|
|
+
|
|
|
+func inspectPEM(input string) ([]gin.H, error) {
|
|
|
+ var blocks []gin.H
|
|
|
+ remaining := []byte(input)
|
|
|
+
|
|
|
+ for len(strings.TrimSpace(string(remaining))) > 0 {
|
|
|
+ block, rest := pem.Decode(remaining)
|
|
|
+ if block == nil {
|
|
|
+ return nil, errors.New("unable to decode PEM data")
|
|
|
+ }
|
|
|
+
|
|
|
+ inspected := gin.H{
|
|
|
+ "pemType": block.Type,
|
|
|
+ }
|
|
|
+
|
|
|
+ switch block.Type {
|
|
|
+ case "CERTIFICATE":
|
|
|
+ cert, err := x509.ParseCertificate(block.Bytes)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ inspected["kind"] = "certificate"
|
|
|
+ inspected["subject"] = cert.Subject.String()
|
|
|
+ inspected["issuer"] = cert.Issuer.String()
|
|
|
+ inspected["serialNumber"] = cert.SerialNumber.String()
|
|
|
+ inspected["notBefore"] = cert.NotBefore.Format(time.RFC3339)
|
|
|
+ inspected["notAfter"] = cert.NotAfter.Format(time.RFC3339)
|
|
|
+ inspected["dnsNames"] = cert.DNSNames
|
|
|
+ inspected["isCA"] = cert.IsCA
|
|
|
+ inspected["signatureAlgorithm"] = cert.SignatureAlgorithm.String()
|
|
|
+ inspected["publicKeyAlgorithm"] = cert.PublicKeyAlgorithm.String()
|
|
|
+ case "CERTIFICATE REQUEST", "NEW CERTIFICATE REQUEST":
|
|
|
+ csr, err := x509.ParseCertificateRequest(block.Bytes)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ inspected["kind"] = "certificate_request"
|
|
|
+ inspected["subject"] = csr.Subject.String()
|
|
|
+ inspected["dnsNames"] = csr.DNSNames
|
|
|
+ inspected["emailAddresses"] = csr.EmailAddresses
|
|
|
+ inspected["signatureAlgorithm"] = csr.SignatureAlgorithm.String()
|
|
|
+ inspected["publicKeyAlgorithm"] = csr.PublicKeyAlgorithm.String()
|
|
|
+ default:
|
|
|
+ keyInfo, err := inspectKeyBlock(block)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ for key, value := range keyInfo {
|
|
|
+ inspected[key] = value
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ blocks = append(blocks, inspected)
|
|
|
+ remaining = rest
|
|
|
+ }
|
|
|
+
|
|
|
+ if len(blocks) == 0 {
|
|
|
+ return nil, errors.New("no PEM blocks found")
|
|
|
+ }
|
|
|
+
|
|
|
+ return blocks, nil
|
|
|
+}
|
|
|
+
|
|
|
+func inspectKeyBlock(block *pem.Block) (gin.H, error) {
|
|
|
+ switch block.Type {
|
|
|
+ case "RSA PRIVATE KEY":
|
|
|
+ key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ return gin.H{
|
|
|
+ "kind": "private_key",
|
|
|
+ "algorithm": "RSA",
|
|
|
+ "size": key.N.BitLen(),
|
|
|
+ }, nil
|
|
|
+ case "EC PRIVATE KEY":
|
|
|
+ key, err := x509.ParseECPrivateKey(block.Bytes)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ return gin.H{
|
|
|
+ "kind": "private_key",
|
|
|
+ "algorithm": "EC",
|
|
|
+ "curve": key.Curve.Params().Name,
|
|
|
+ }, nil
|
|
|
+ case "PRIVATE KEY":
|
|
|
+ key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ return describePrivateKey(key), nil
|
|
|
+ case "PUBLIC KEY":
|
|
|
+ key, err := x509.ParsePKIXPublicKey(block.Bytes)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ return describePublicKey(key), nil
|
|
|
+ default:
|
|
|
+ return nil, fmt.Errorf("unsupported PEM block type: %s", block.Type)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func describePrivateKey(key any) gin.H {
|
|
|
+ switch typed := key.(type) {
|
|
|
+ case *rsa.PrivateKey:
|
|
|
+ return gin.H{
|
|
|
+ "kind": "private_key",
|
|
|
+ "algorithm": "RSA",
|
|
|
+ "size": typed.N.BitLen(),
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ return gin.H{
|
|
|
+ "kind": "private_key",
|
|
|
+ "algorithm": reflect.TypeOf(key).String(),
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func describePublicKey(key any) gin.H {
|
|
|
+ switch typed := key.(type) {
|
|
|
+ case *rsa.PublicKey:
|
|
|
+ return gin.H{
|
|
|
+ "kind": "public_key",
|
|
|
+ "algorithm": "RSA",
|
|
|
+ "size": typed.N.BitLen(),
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ return gin.H{
|
|
|
+ "kind": "public_key",
|
|
|
+ "algorithm": reflect.TypeOf(key).String(),
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func splitCSV(input string) []string {
|
|
|
+ parts := strings.Split(input, ",")
|
|
|
+ values := make([]string, 0, len(parts))
|
|
|
+ for _, part := range parts {
|
|
|
+ trimmed := strings.TrimSpace(part)
|
|
|
+ if trimmed != "" {
|
|
|
+ values = append(values, trimmed)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return values
|
|
|
+}
|
|
|
+
|
|
|
+func normalizeHost(input string) string {
|
|
|
+ value := strings.TrimSpace(input)
|
|
|
+ value = strings.TrimPrefix(value, "https://")
|
|
|
+ value = strings.TrimPrefix(value, "http://")
|
|
|
+ value = strings.Trim(value, "/")
|
|
|
+ if host, _, err := net.SplitHostPort(value); err == nil {
|
|
|
+ return host
|
|
|
+ }
|
|
|
+ if parsed, err := url.Parse("https://" + value); err == nil {
|
|
|
+ return parsed.Hostname()
|
|
|
+ }
|
|
|
+ return value
|
|
|
+}
|
|
|
+
|
|
|
+func normalizeURL(input string) (*url.URL, error) {
|
|
|
+ value := strings.TrimSpace(input)
|
|
|
+ if value == "" {
|
|
|
+ return nil, errors.New("url is required")
|
|
|
+ }
|
|
|
+ if !strings.Contains(value, "://") {
|
|
|
+ value = "https://" + value
|
|
|
+ }
|
|
|
+ parsed, err := url.Parse(value)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ if parsed.Hostname() == "" {
|
|
|
+ return nil, errors.New("url must include a hostname")
|
|
|
+ }
|
|
|
+ return parsed, nil
|
|
|
+}
|
|
|
+
|
|
|
+func tlsVersionName(version uint16) string {
|
|
|
+ switch version {
|
|
|
+ case tls.VersionTLS10:
|
|
|
+ return "TLS 1.0"
|
|
|
+ case tls.VersionTLS11:
|
|
|
+ return "TLS 1.1"
|
|
|
+ case tls.VersionTLS12:
|
|
|
+ return "TLS 1.2"
|
|
|
+ case tls.VersionTLS13:
|
|
|
+ return "TLS 1.3"
|
|
|
+ default:
|
|
|
+ return fmt.Sprintf("0x%x", version)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func normalizeKeySize(value int) int {
|
|
|
+ switch value {
|
|
|
+ case 0, 2048:
|
|
|
+ return 2048
|
|
|
+ case 3072:
|
|
|
+ return 3072
|
|
|
+ case 4096:
|
|
|
+ return 4096
|
|
|
+ default:
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func sanitizeFilename(value string) string {
|
|
|
+ trimmed := strings.TrimSpace(strings.ToLower(value))
|
|
|
+ if trimmed == "" {
|
|
|
+ return "certificate"
|
|
|
+ }
|
|
|
+
|
|
|
+ var builder strings.Builder
|
|
|
+ lastDash := false
|
|
|
+ for _, r := range trimmed {
|
|
|
+ isAlphaNum := (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9')
|
|
|
+ if isAlphaNum {
|
|
|
+ builder.WriteRune(r)
|
|
|
+ lastDash = false
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if !lastDash {
|
|
|
+ builder.WriteRune('-')
|
|
|
+ lastDash = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ result := strings.Trim(builder.String(), "-")
|
|
|
+ if result == "" {
|
|
|
+ return "certificate"
|
|
|
+ }
|
|
|
+ return result
|
|
|
+}
|
|
|
+
|
|
|
+func getenv(key, fallback string) string {
|
|
|
+ value := strings.TrimSpace(os.Getenv(key))
|
|
|
+ if value == "" {
|
|
|
+ return fallback
|
|
|
+ }
|
|
|
+ return value
|
|
|
+}
|