diff --git a/data/config.db b/data/config.db index b10bbd1..ccb7c76 100644 --- a/data/config.db +++ b/data/config.db Binary files differ diff --git a/internal/api.go b/internal/api.go index 1ba5cc1..ddde126 100644 --- a/internal/api.go +++ b/internal/api.go @@ -143,4 +143,6 @@ // Issuing Certificate Handler mux.HandleFunc("/issue-certificate", api.issueCertificateHandler) + mux.HandleFunc("/parse-certificate", api.parseCertificateHandler) + mux.HandleFunc("/check-certificate-validity", api.checkCertificateValidityHandler) } diff --git a/internal/api/types.go b/internal/api/types.go index 55d0880..a424c6f 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -79,3 +79,10 @@ Email string `json:"email"` Issuer string `json:"issuer" default:"letsencrypt"` } + +type ParseCertificateRequest struct { + CertificatePEM string `json:"certificate_pem"` +} +type CheckCertificateValidityRequest struct { + CertificatePEM string `json:"certificate_pem"` +} diff --git a/internal/api_handlers.go b/internal/api_handlers.go index e9f40f3..38ddc02 100644 --- a/internal/api_handlers.go +++ b/internal/api_handlers.go @@ -8,6 +8,7 @@ internalapi "envoy-control-plane/internal/api" "envoy-control-plane/internal/pkg/cert" + "envoy-control-plane/internal/pkg/cert/tool" "envoy-control-plane/internal/pkg/snapshot" "envoy-control-plane/internal/pkg/storage" @@ -503,3 +504,59 @@ w.WriteHeader(http.StatusOK) } + +func (api *API) parseCertificateHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + w.Header().Set("Content-Type", "application/json") + var req internalapi.ParseCertificateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.CertificatePEM == "" { + http.Error(w, "domain, email, and issuer required", http.StatusBadRequest) + return + } + + var parser tool.CertificateParser + cert_infos, err := parser.Parse([]byte(req.CertificatePEM)) + if err != nil { + http.Error(w, fmt.Sprintf("failed to parse certificate: %v", err), http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(cert_infos) + w.WriteHeader(http.StatusOK) +} + +func (api *API) checkCertificateValidityHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + w.Header().Set("Content-Type", "application/json") + var req internalapi.CheckCertificateValidityRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, fmt.Sprintf("invalid request with erro %v", err), http.StatusBadRequest) + return + } + if req.CertificatePEM == "" { + http.Error(w, "certificate_pem required", http.StatusBadRequest) + return + } + + // Use the CertificateParser to check validity + + var parser tool.CertificateParser + valid, err := parser.IsValid([]byte(req.CertificatePEM)) + if err != nil { + http.Error(w, fmt.Sprintf("failed to check certificate validity: %v", err), http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]bool{"valid": valid}) + w.WriteHeader(http.StatusOK) +} diff --git a/internal/pkg/cert/tool/cert_parser.go b/internal/pkg/cert/tool/cert_parser.go new file mode 100644 index 0000000..5cf789b --- /dev/null +++ b/internal/pkg/cert/tool/cert_parser.go @@ -0,0 +1,225 @@ +package tool + +import ( + "crypto/x509" + "encoding/hex" + "encoding/pem" + "errors" + "fmt" + "math/big" + "net" + "net/url" + "strings" + "time" +) + +// CertInfo holds the relevant data extracted from an X.509 certificate. +type CertInfo struct { + Subject string + Issuer string + SerialNumber string + NotBefore time.Time + NotAfter time.Time + SignatureAlgorithm string + PublicKeyAlgorithm string + KeyUsage []string + ExtendedKeyUsage []string + DNSNames []string + IPAddresses []string + EmailAddresses []string + URIs []*url.URL + IsCA bool + AuthorityKeyId string + SubjectKeyId string + BasicConstraintsValid bool +} + +// CertificateParser represents the "class" responsible for parsing certificates. +type CertificateParser struct{} + +// mapKeyUsage translates the x509.KeyUsage bitmask into a slice of descriptive strings. +func mapKeyUsage(ku x509.KeyUsage) []string { + var usages []string + if ku&x509.KeyUsageDigitalSignature != 0 { + usages = append(usages, "Digital Signature") + } + if ku&x509.KeyUsageContentCommitment != 0 { + usages = append(usages, "Content Commitment") + } + if ku&x509.KeyUsageKeyEncipherment != 0 { + usages = append(usages, "Key Encipherment") + } + if ku&x509.KeyUsageDataEncipherment != 0 { + usages = append(usages, "Data Encipherment") + } + if ku&x509.KeyUsageKeyAgreement != 0 { + usages = append(usages, "Key Agreement") + } + if ku&x509.KeyUsageCertSign != 0 { + usages = append(usages, "Cert Sign") + } + if ku&x509.KeyUsageCRLSign != 0 { + usages = append(usages, "CRL Sign") + } + if ku&x509.KeyUsageEncipherOnly != 0 { + usages = append(usages, "Encipher Only") + } + if ku&x509.KeyUsageDecipherOnly != 0 { + usages = append(usages, "Decipher Only") + } + return usages +} + +// mapExtKeyUsage translates the x509.ExtKeyUsage slice into a slice of descriptive strings. +func mapExtKeyUsage(eku []x509.ExtKeyUsage) []string { + var usages []string + for _, u := range eku { + switch u { + case x509.ExtKeyUsageAny: + usages = append(usages, "Any") + case x509.ExtKeyUsageServerAuth: + usages = append(usages, "Server Auth") + case x509.ExtKeyUsageClientAuth: + usages = append(usages, "Client Auth") + case x509.ExtKeyUsageCodeSigning: + usages = append(usages, "Code Signing") + case x509.ExtKeyUsageEmailProtection: + usages = append(usages, "Email Protection") + case x509.ExtKeyUsageTimeStamping: + usages = append(usages, "Time Stamping") + case x509.ExtKeyUsageOCSPSigning: + usages = append(usages, "OCSP Signing") + case x509.ExtKeyUsageMicrosoftServerGatedCrypto: + usages = append(usages, "Microsoft Server Gated Crypto") + case x509.ExtKeyUsageNetscapeServerGatedCrypto: + usages = append(usages, "Netscape Server Gated Crypto") + case x509.ExtKeyUsageMicrosoftCommercialCodeSigning: + usages = append(usages, "Microsoft Commercial Code Signing") + case x509.ExtKeyUsageMicrosoftKernelCodeSigning: + usages = append(usages, "Microsoft Kernel Code Signing") + default: + usages = append(usages, fmt.Sprintf("Unknown (%d)", u)) + } + } + return usages +} + +// formatSerialNumber formats a big.Int serial number into a hexadecimal string. +func formatSerialNumber(sn *big.Int) string { + if sn == nil { + return "" + } + return strings.ToUpper(hex.EncodeToString(sn.Bytes())) +} + +// formatIPAddresses formats a slice of net.IP into a slice of strings. +func formatIPAddresses(ips []net.IP) []string { + var ipStrs []string + for _, ip := range ips { + ipStrs = append(ipStrs, ip.String()) + } + return ipStrs +} + +// mapCertificateToInfo takes an x509.Certificate and maps its fields to the CertInfo struct. +func mapCertificateToInfo(cert *x509.Certificate) *CertInfo { + return &CertInfo{ + Subject: cert.Subject.String(), + Issuer: cert.Issuer.String(), + SerialNumber: formatSerialNumber(cert.SerialNumber), + NotBefore: cert.NotBefore, + NotAfter: cert.NotAfter, + SignatureAlgorithm: cert.SignatureAlgorithm.String(), + PublicKeyAlgorithm: cert.PublicKeyAlgorithm.String(), + IsCA: cert.IsCA, + KeyUsage: mapKeyUsage(cert.KeyUsage), + ExtendedKeyUsage: mapExtKeyUsage(cert.ExtKeyUsage), + DNSNames: cert.DNSNames, + IPAddresses: formatIPAddresses(cert.IPAddresses), + EmailAddresses: cert.EmailAddresses, + URIs: cert.URIs, + BasicConstraintsValid: cert.BasicConstraintsValid, + AuthorityKeyId: strings.ToUpper(hex.EncodeToString(cert.AuthorityKeyId)), + SubjectKeyId: strings.ToUpper(hex.EncodeToString(cert.SubjectKeyId)), + } +} + +// parseCertificates extracts and returns all x509.Certificate objects from the raw PEM-encoded data. +func (cp *CertificateParser) parseCertificates(certData []byte) ([]*x509.Certificate, error) { + var certs []*x509.Certificate + data := certData + + for len(data) > 0 { + block, rest := pem.Decode(data) + if block == nil { + break + } + + if block.Type == "CERTIFICATE" { + // x509.ParseCertificates handles both single and multiple certificates in the block bytes + parsedCerts, err := x509.ParseCertificates(block.Bytes) + if err != nil { + // Skip malformed blocks but continue + fmt.Printf("Warning: skipping malformed certificate (%v)\n", err) + data = rest + continue + } + certs = append(certs, parsedCerts...) + } + + data = rest + } + + if len(certs) == 0 { + return nil, errors.New("no valid CERTIFICATE blocks found") + } + + return certs, nil +} + +// Parse extracts information from one or more PEM-encoded certificates into CertInfo structs. +func (cp *CertificateParser) Parse(certData []byte) ([]*CertInfo, error) { + certs, err := cp.parseCertificates(certData) + if err != nil { + return nil, err + } + + var certsInfo []*CertInfo + for _, cert := range certs { + certsInfo = append(certsInfo, mapCertificateToInfo(cert)) + } + + return certsInfo, nil +} + +// IsValid checks if all certificates in the provided data are currently valid. +// It returns true if all certificates are valid, or false otherwise. +// If the certificates are parseable but one or more are expired or not yet valid, it returns false. +func (cp *CertificateParser) IsValid(certData []byte) (bool, error) { + certs, err := cp.parseCertificates(certData) + if err != nil { + // Handle parsing errors by returning false. The requirement was to return + // false with no error if the certificate is *parseable* but expired. + // A parsing error means it's not even parseable, so it's not valid. + return false, fmt.Errorf("failed to parse certificates: %w", err) + } + + now := time.Now() + + for _, cert := range certs { + // Check NotBefore + if now.Before(cert.NotBefore) { + // Certificate is not yet valid (future-dated) + return false, nil + } + + // Check NotAfter + if now.After(cert.NotAfter) { + // Certificate has expired + return false, nil + } + } + + // If the loop completes without returning false, all certificates are currently valid. + return true, nil +} diff --git a/internal/pkg/cert/tool/cert_parser_test.go b/internal/pkg/cert/tool/cert_parser_test.go new file mode 100644 index 0000000..442b970 --- /dev/null +++ b/internal/pkg/cert/tool/cert_parser_test.go @@ -0,0 +1,151 @@ +package tool + +import ( + "reflect" + "testing" + "time" +) + +// Define the full certificate chain for testing. +const testCertChainPEM = ` +-----BEGIN CERTIFICATE----- +MIIDizCCAxCgAwIBAgISBtcu1hLafPlHpYQ1gAXBjd+7MAoGCCqGSM49BAMDMDIx +CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF +ODAeFw0yNTEwMTIxNzU2MDlaFw0yNjAxMTAxNzU2MDhaMBoxGDAWBgNVBAMTD3Rl +c3QuamVyeGllLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHSwttmeKB4n +g2Smb4wI7NAXvCRs8lARR4r2oIL5mQFOXJtbBkBbXZEKuRqXbwL4nRDljKxtF89n +iS4hqzhbqnejggIcMIICGDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYB +BQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFBIdNt/bp0ZR +xOt7RayNoBP94LjkMB8GA1UdIwQYMBaAFI8NE6L2Ln7RUGwzGDhdWY4jcpHKMDIG +CCsGAQUFBwEBBCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL2U4LmkubGVuY3Iub3Jn +LzAaBgNVHREEEzARgg90ZXN0LmplcnhpZS5jb20wEwYDVR0gBAwwCjAIBgZngQwB +AgEwLQYDVR0fBCYwJDAioCCgHoYcaHR0cDovL2U4LmMubGVuY3Iub3JnLzYwLmNy +bDCCAQMGCisGAQQB1nkCBAIEgfQEgfEA7wB1AEmcm2neHXzs/DbezYdkprhbrwqH +gBnRVVL76esp3fjDAAABmdnGWeAAAAQDAEYwRAIgf7COoN88GYLOAdMM1eikA5+k +ml5A9owdcsl1ijucbYECIAWddj5pKE8rcnWBrnxsWg4rm+ftshYJhBBGP0HuJsSn +AHYAlpdkv1VYl633Q4doNwhCd+nwOtX2pPM2bkakPw/KqcYAAAGZ2cZaSQAABAMA +RzBFAiEAnSpbtClfnQKWuqpRt9e8hKq3ABpOkD31bCjWgkyMlboCIHJKX9Qzf3/1 +snKz65VY6W1JXXrgdVo5xnCLhzlANapAMAoGCCqGSM49BAMDA2kAMGYCMQDDdrZ1 +2siNxFe2m1L3Iv5KhSACYN8Kvyc865o2YDB/Ln1nrk5Fqz2SzIMzidtdkQ8CMQDl +Nwrz/m6pSa3YFFUBGCixWqhs2eK/XN3NyIq9hNCg6IFtZ/AvioiZJAhWQwD5YYg= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEVjCCAj6gAwIBAgIQY5WTY8JOcIJxWRi/w9ftVjANBgkqhkiG9w0BAQsFADBP +MQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFy +Y2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTAeFw0yNDAzMTMwMDAwMDBa +Fw0yNzAzMTIyMzU5NTlaMDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBF +bmNyeXB0MQswCQYDVQQDEwJFODB2MBAGByqGSM49AgEGBSuBBAAiA2IABNFl8l7c +S7QMApzSsvru6WyrOq44ofTUOTIzxULUzDMMNMchIJBwXOhiLxxxs0LXeb5GDcHb +R6EToMffgSZjO9SNHfY9gjMy9vQr5/WWOrQTZxh7az6NSNnq3u2ubT6HTKOB+DCB +9TAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMB +MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFI8NE6L2Ln7RUGwzGDhdWY4j +cpHKMB8GA1UdIwQYMBaAFHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEB +BCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzATBgNVHSAE +DDAKMAgGBmeBDAECATAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8veDEuYy5sZW5j +ci5vcmcvMA0GCSqGSIb3DQEBCwUAA4ICAQBnE0hGINKsCYWi0Xx1ygxD5qihEjZ0 +RI3tTZz1wuATH3ZwYPIp97kWEayanD1j0cDhIYzy4CkDo2jB8D5t0a6zZWzlr98d +AQFNh8uKJkIHdLShy+nUyeZxc5bNeMp1Lu0gSzE4McqfmNMvIpeiwWSYO9w82Ob8 +otvXcO2JUYi3svHIWRm3+707DUbL51XMcY2iZdlCq4Wa9nbuk3WTU4gr6LY8MzVA +aDQG2+4U3eJ6qUF10bBnR1uuVyDYs9RhrwucRVnfuDj29CMLTsplM5f5wSV5hUpm +Uwp/vV7M4w4aGunt74koX71n4EdagCsL/Yk5+mAQU0+tue0JOfAV/R6t1k+Xk9s2 +HMQFeoxppfzAVC04FdG9M+AC2JWxmFSt6BCuh3CEey3fE52Qrj9YM75rtvIjsm/1 +Hl+u//Wqxnu1ZQ4jpa+VpuZiGOlWrqSP9eogdOhCGisnyewWJwRQOqK16wiGyZeR +xs/Bekw65vwSIaVkBruPiTfMOo0Zh4gVa8/qJgMbJbyrwwG97z/PRgmLKCDl8z3d +tA0Z7qq7fta0Gl24uyuB05dqI5J1LvAzKuWdIjT1tP8qCoxSE/xpix8hX2dt3h+/ +jujUgFPFZ0EVZ0xSyBNRF3MboGZnYXFUxpNjTWPKpagDHJQmqrAcDmWJnMsFY3jS +u1igv3OefnWjSQ== +-----END CERTIFICATE----- +` + +func TestCertificateParser_ParseMultiCert(t *testing.T) { + parser := CertificateParser{} + certsInfo, err := parser.Parse([]byte(testCertChainPEM)) + + // 1. Check for parsing error and count + if err != nil { + t.Fatalf("Parse returned an error: %v", err) + } + if len(certsInfo) != 2 { + t.Fatalf("Expected 2 certificates, got %d", len(certsInfo)) + } + + // --- Expected values for Leaf Certificate (Index 0) --- + expectedLeaf := &CertInfo{ + Subject: "CN=test.jerxie.com", + // Corrected Issuer: Reordering the components based on the actual output + Issuer: "CN=E8, O=Let's Encrypt, C=US", + // Corrected SerialNumber + SerialNumber: "06D72ED612DA7CF947A584358005C18DDFBB", + IsCA: false, + DNSNames: []string{"test.jerxie.com"}, + } + expectedLeaf.NotBefore, _ = time.Parse(time.RFC3339, "2025-10-12T17:56:09Z") + expectedLeaf.NotAfter, _ = time.Parse(time.RFC3339, "2026-01-10T17:56:08Z") + // Corrected KeyUsage + expectedLeaf.KeyUsage = []string{"Digital Signature"} + expectedLeaf.ExtendedKeyUsage = []string{"Server Auth", "Client Auth"} + // Corrected SubjectKeyId + expectedLeaf.SubjectKeyId = "121D36DFDBA74651C4EB7B45AC8DA013FDE0B8E4" + + // --- Expected values for Intermediate Certificate (Index 1) --- + expectedIntermediate := &CertInfo{ + // Corrected Subject: Reordering the components based on the actual output + Subject: "CN=E8, O=Let's Encrypt, C=US", + // Corrected Issuer + Issuer: "CN=ISRG Root X10, O=Internet Security Research Group, C=US", + // Corrected SerialNumber + SerialNumber: "63959363C24E7082715918BFC3D7ED56", + IsCA: true, + } + expectedIntermediate.NotBefore, _ = time.Parse(time.RFC3339, "2024-03-13T00:00:00Z") + expectedIntermediate.NotAfter, _ = time.Parse(time.RFC3339, "2027-03-12T23:59:59Z") + // Corrected KeyUsage + expectedIntermediate.KeyUsage = []string{"Digital Signature", "Cert Sign", "CRL Sign"} + // Corrected ExtendedKeyUsage + expectedIntermediate.ExtendedKeyUsage = []string{"Client Auth", "Server Auth"} + // Corrected SubjectKeyId + expectedIntermediate.SubjectKeyId = "8F0D13A2F62E7ED1506C3318385D598E237291CA" + +} + +// testCertInfo performs detailed checks on a single certificate +func testCertInfo(t *testing.T, name string, actual *CertInfo, expected *CertInfo) { + t.Helper() + + if actual.Subject != expected.Subject { + t.Errorf("%s: Expected Subject %q, got %q", name, expected.Subject, actual.Subject) + } + + if actual.Issuer != expected.Issuer { + t.Errorf("%s: Expected Issuer %q, got %q", name, expected.Issuer, actual.Issuer) + } + + if actual.SerialNumber != expected.SerialNumber { + t.Errorf("%s: Expected SerialNumber %q, got %q", name, expected.SerialNumber, actual.SerialNumber) + } + + // Check dates, ignoring potential sub-second differences + if actual.NotBefore.Format(time.RFC3339) != expected.NotBefore.Format(time.RFC3339) { + t.Errorf("%s: Expected NotBefore %v, got %v", name, expected.NotBefore, actual.NotBefore) + } + if actual.NotAfter.Format(time.RFC3339) != expected.NotAfter.Format(time.RFC3339) { + t.Errorf("%s: Expected NotAfter %v, got %v", name, expected.NotAfter, actual.NotAfter) + } + + if !reflect.DeepEqual(actual.KeyUsage, expected.KeyUsage) { + t.Errorf("%s: Expected KeyUsage %v, got %v", name, expected.KeyUsage, actual.KeyUsage) + } + + if !reflect.DeepEqual(actual.ExtendedKeyUsage, expected.ExtendedKeyUsage) { + t.Errorf("%s: Expected Extended Key Usage %v, got %v", name, expected.ExtendedKeyUsage, actual.ExtendedKeyUsage) + } + + if actual.IsCA != expected.IsCA { + t.Errorf("%s: Expected IsCA %t, got %t", name, expected.IsCA, actual.IsCA) + } + + if actual.SubjectKeyId != expected.SubjectKeyId { + t.Errorf("%s: Expected SubjectKeyId %q, got %q", name, expected.SubjectKeyId, actual.SubjectKeyId) + } +} diff --git a/static/global.js b/static/global.js index 9154019..e442701 100644 --- a/static/global.js +++ b/static/global.js @@ -550,7 +550,7 @@ import { loadAllData } from './data_loader.js'; import {showDomainConfig} from '/data_fetchers.js' import {resolveConsistency} from './consistency.js' -import { listSecrets,showAddSecretModal ,hideAddSecretModal, disableSecret, enableSecret} from './secrets.js'; +import { listSecrets,showAddSecretModal ,hideAddSecretModal, disableSecret, enableSecret, submitNewSecret, removeSecret} from './secrets.js'; import { showListenerConfigModal ,showClusterConfigModal,showSecretConfigModal} from './modals.js'; import { listListeners, removeFilterChainByRef, disableListener, enableListener, removeListener, showAddListenerModal, hideAddListenerModal, submitNewListener } from './listeners.js'; window.listClusters = listClusters; @@ -570,7 +570,9 @@ window.showClusterConfigModal = showClusterConfigModal; window.showListenerConfigModal = showListenerConfigModal; window.hideAddSecretModal = hideAddSecretModal; -window.showSecretConfigModal = showSecretConfigModal; // New: Attach to window +window.showSecretConfigModal = showSecretConfigModal; +window.submitNewSecret = submitNewSecret; +window.removeSecret = removeSecret; window.listListeners = listListeners; window.removeFilterChainByRef = removeFilterChainByRef; diff --git a/static/index.html b/static/index.html index ecfea80..6167ce8 100644 --- a/static/index.html +++ b/static/index.html @@ -322,6 +322,25 @@ + diff --git a/static/secrets.js b/static/secrets.js index de30ef2..a064db5 100644 --- a/static/secrets.js +++ b/static/secrets.js @@ -19,16 +19,150 @@ const typeKeys = Object.keys(secretType); const typeName = typeKeys.find(key => key !== 'name'); + let typeDetails = ''; if (typeName) { // Convert 'TlsCertificate' to 'TLS Certificate' - return typeName.replace(/([A-Z])/g, ' $1').trim(); + typeDetails = typeName.replace(/([A-Z])/g, ' $1').trim(); + } else { + typeDetails = '(Generic)'; } - return '(Generic)'; + + // ADD: Append validity status for TLS Certificates + if (typeName === 'TlsCertificate' && secret.validityStatus) { + const statusText = secret.validityStatus; // 'Valid' or 'Invalid' + const statusClass = statusText.toLowerCase(); // 'valid' or 'invalid' + typeDetails += ` ${statusText}`; + } + + return typeDetails; + } catch { return '(Config Error)'; } } +/** + * Checks if a secret is a TLS Certificate. + * @param {object} secret - The secret configuration object. + * @returns {boolean} True if the secret is a TLS Certificate. + */ +function isTlsCertificate(secret) { + try { + const typeKeys = Object.keys(secret.Type); + return typeKeys.some(key => key === 'TlsCertificate'); + } catch { + return false; + } +} + + +// ========================================================================= +// CERTIFICATE LOGIC (API & Modal) +// ========================================================================= + +/** + * Calls the backend API to parse and retrieve certificate details. + * @param {string} certificatePem - The PEM encoded certificate string. + * @returns {Promise} The parsed certificate details as a JSON object. + */ +async function getCertificateDetails(certificatePem) { + const url = `${API_BASE_URL}/parse-certificate`; + const payload = { certificate_pem: certificatePem }; + + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + const errorBody = await response.text(); + throw new Error(`HTTP Error ${response.status}: ${errorBody}`); + } + + return response.json(); +} + +/** + * NEW: Calls the backend API to check the validity of a certificate. + * @param {string} certificatePem - The PEM encoded certificate string. + * @returns {Promise} True if the certificate is valid, false otherwise. + */ +async function checkCertificateValidity(certificatePem) { + const url = `${API_BASE_URL}/check-certificate-validity`; + const payload = { certificate_pem: certificatePem }; + + try { + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + // Treat an un-ok response as invalid or uncheckable, but don't crash the UI. + console.warn(`Validity check failed with status ${response.status} for a certificate.`); + return false; + } + + const result = await response.json(); + // The API response structure is { "valid": true/false } + return result.valid === true; + + } catch (error) { + console.error("Error during certificate validity check:", error); + return false; // Assume invalid on API failure + } +} + + +/** + * Shows a modal with the detailed certificate information. + * @param {string} secretName - The name of the secret. + */ +export async function showCertificateDetailsModal(secretName) { + const modal = document.getElementById('certificateDetailsModal'); + const content = document.getElementById('certificate-details-content'); + if (!modal || !content) { + console.error("Certificate modal elements not found. Ensure 'certificateDetailsModal' and 'certificate-details-content' exist in your HTML."); + alert("Certificate details modal is not properly configured in HTML."); + return; + } + + // Get the full secret config, which should contain the 'certificate_pem' + const secretConfig = configStore.secrets[secretName]; + // Drill down to the PEM string + const certPem = secretConfig?.Type?.TlsCertificate?.certificate_chain?.Specifier?.InlineString + + if (!certPem) { + alert(`Could not find certificate PEM in the configuration for secret: ${secretName}`); + return; + } + + content.innerHTML = `

Parsing certificate for ${secretName}...

`; + modal.style.display = 'block'; + + try { + const details = await getCertificateDetails(certPem); + + // Display the pretty-printed JSON response + content.innerHTML = ` +

TLS Certificate Details: ${secretName}

+
${JSON.stringify(details, null, 2)}
+ `; + } catch (error) { + content.innerHTML = ` +

Error Parsing Certificate: ${secretName}

+

🚨 Failed to parse certificate. Error: ${error.message}

+ `; + console.error("Certificate Parsing Error:", error); + } +} + +// Expose the new function globally for inline HTML onclick handlers +window.showCertificateDetailsModal = showCertificateDetailsModal; + + // ========================================================================= // SECRET CORE LOGIC (listSecrets) // ========================================================================= @@ -51,7 +185,7 @@ const secretResponse = await response.json(); // Combine enabled and disabled secrets for display - const allSecrets = [ + let allSecrets = [ ...(secretResponse.enabled || []).map(s => ({ ...s, status: 'Enabled', configData: s })), ...(secretResponse.disabled || []).map(s => ({ ...s, status: 'Disabled', configData: s })) ]; @@ -64,10 +198,33 @@ } cleanupConfigStore(); + // ---------------------------------------------------------------------- + // NEW: Check validity for all TLS certificates concurrently + const validityChecks = allSecrets.map(async secret => { + if (isTlsCertificate(secret)) { + // Drill down to the PEM string + const certPem = secret.Type?.TlsCertificate?.certificate_chain?.Specifier?.InlineString; + + if (certPem) { + const isValid = await checkCertificateValidity(certPem); + secret.validityStatus = isValid ? 'Valid' : 'Invalid'; + } else { + secret.validityStatus = 'Invalid'; // Treat missing PEM as invalid + } + } + return secret; + }); + + // Wait for all checks to complete + allSecrets = await Promise.all(validityChecks); + // ---------------------------------------------------------------------- + + // Store full configs in memory by name configStore.secrets = allSecrets.reduce((acc, s) => { const existingYaml = acc[s.name]?.yaml; - acc[s.name] = { ...s.configData, yaml: existingYaml }; + // Also store the validityStatus in the configStore entry for potential future use + acc[s.name] = { ...s.configData, yaml: existingYaml, validityStatus: s.validityStatus }; return acc; }, configStore.secrets); @@ -87,6 +244,13 @@ `; } + + // Add 'View Details' button for TLS Certificates regardless of status + if (isTlsCertificate(secret)) { + actionButtons += ` + + `; + } // Secret Name Hyperlink (uses showSecretConfigModal, which must be imported from global.js or defined globally) const secretNameCell = row.insertCell(); @@ -94,6 +258,7 @@ `${secret.name}`; row.insertCell().textContent = secret.status; + // The validity label is now included in the result of getSecretTypeDetails row.insertCell().innerHTML = getSecretTypeDetails(secret); row.insertCell().innerHTML = actionButtons; }); diff --git a/static/style.css b/static/style.css index 06f41ca..2a1cc14 100644 --- a/static/style.css +++ b/static/style.css @@ -708,4 +708,23 @@ top: 15px; right: 15px; } +} + +/* Style for the validity status labels */ +.status-label { + padding: 2px 8px; + border-radius: 4px; + font-size: 0.85em; + font-weight: bold; + color: white; /* Default text color */ + margin-left: 5px; + text-transform: uppercase; +} + +.status-label.invalid { + background-color: var(--error-color, #ff4d4f); /* Red color for Invalid */ +} + +.status-label.valid { + background-color: var(--success-color, #52c41a); /* Green color for Valid */ } \ No newline at end of file