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 @@ +