Add SSL certificate checker
Implement SSL certificate checking functionality
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
|
||||
package operations
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
// extractSANs extracts Subject Alternative Names from certificate
|
||||
func (op *SSLOperation) extractSANs(cert *x509.Certificate) []string {
|
||||
sans := make([]string, 0)
|
||||
|
||||
// Add DNS names
|
||||
sans = append(sans, cert.DNSNames...)
|
||||
|
||||
// Add IP addresses
|
||||
for _, ip := range cert.IPAddresses {
|
||||
sans = append(sans, ip.String())
|
||||
}
|
||||
|
||||
// Add email addresses
|
||||
sans = append(sans, cert.EmailAddresses...)
|
||||
|
||||
// Add URIs
|
||||
for _, uri := range cert.URIs {
|
||||
sans = append(sans, uri.String())
|
||||
}
|
||||
|
||||
return sans
|
||||
}
|
||||
|
||||
// getResolvedIP resolves the domain to its IP address
|
||||
func (op *SSLOperation) getResolvedIP(hostname string) string {
|
||||
ips, err := net.LookupIP(hostname)
|
||||
if err != nil || len(ips) == 0 {
|
||||
return ""
|
||||
}
|
||||
return ips[0].String()
|
||||
}
|
||||
|
||||
// getCertificateAlgorithm returns detailed algorithm information
|
||||
func (op *SSLOperation) getCertificateAlgorithm(cert *x509.Certificate) string {
|
||||
algorithm := cert.SignatureAlgorithm.String()
|
||||
|
||||
// Add key size information if available
|
||||
switch pub := cert.PublicKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
algorithm += fmt.Sprintf(" (RSA %d-bit)", pub.N.BitLen())
|
||||
case *ecdsa.PublicKey:
|
||||
algorithm += fmt.Sprintf(" (ECDSA %d-bit)", pub.Curve.Params().BitSize)
|
||||
}
|
||||
|
||||
return algorithm
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
|
||||
package operations
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service-operation/types"
|
||||
)
|
||||
|
||||
type SSLOperation struct {
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func NewSSLOperation(timeout time.Duration) *SSLOperation {
|
||||
return &SSLOperation{
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (op *SSLOperation) Execute(domain string) (*types.OperationResult, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
// Clean and normalize domain
|
||||
domain = op.normalizeDomain(domain)
|
||||
|
||||
// Validate domain format
|
||||
if domain == "" {
|
||||
return op.createErrorResult(domain, startTime, "domain cannot be empty")
|
||||
}
|
||||
|
||||
// Add port if not present
|
||||
host := domain
|
||||
if !strings.Contains(host, ":") {
|
||||
host = host + ":443"
|
||||
}
|
||||
|
||||
// Set up TLS connection with timeout
|
||||
dialer := &net.Dialer{
|
||||
Timeout: op.timeout,
|
||||
}
|
||||
|
||||
// Create TLS config with proper verification
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: strings.Split(host, ":")[0],
|
||||
InsecureSkipVerify: false,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
// Attempt TLS connection
|
||||
conn, err := tls.DialWithDialer(dialer, "tcp", host, tlsConfig)
|
||||
|
||||
endTime := time.Now()
|
||||
responseTime := endTime.Sub(startTime)
|
||||
|
||||
if err != nil {
|
||||
return &types.OperationResult{
|
||||
Type: types.OperationSSL,
|
||||
Host: strings.Split(host, ":")[0],
|
||||
Success: false,
|
||||
ResponseTime: responseTime,
|
||||
Error: fmt.Sprintf("TLS connection failed: %v", err),
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
}, nil
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Get certificate chain information
|
||||
state := conn.ConnectionState()
|
||||
if len(state.PeerCertificates) == 0 {
|
||||
return &types.OperationResult{
|
||||
Type: types.OperationSSL,
|
||||
Host: strings.Split(host, ":")[0],
|
||||
Success: false,
|
||||
ResponseTime: responseTime,
|
||||
Error: "No certificates found in chain",
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
cert := state.PeerCertificates[0]
|
||||
hostname := strings.Split(host, ":")[0]
|
||||
|
||||
// Perform comprehensive certificate validation
|
||||
validationError := op.validateCertificate(cert, hostname)
|
||||
|
||||
// Calculate days left until expiration
|
||||
daysLeft := int(time.Until(cert.NotAfter).Hours() / 24)
|
||||
|
||||
// Extract Subject Alternative Names
|
||||
sans := op.extractSANs(cert)
|
||||
|
||||
// Get resolved IP address
|
||||
resolvedIP := op.getResolvedIP(hostname)
|
||||
|
||||
// Extract certificate algorithm information
|
||||
algorithm := op.getCertificateAlgorithm(cert)
|
||||
|
||||
// Extract issuer organization (O=) instead of full distinguished name
|
||||
issuerOrganization := op.extractIssuerOrganization(cert.Issuer)
|
||||
|
||||
// Extract subject organization (O=) instead of full distinguished name
|
||||
subjectOrganization := op.extractSubjectOrganization(cert.Subject)
|
||||
|
||||
// Check if certificate is valid (not expired and passes validation)
|
||||
isValid := validationError == nil && time.Now().Before(cert.NotAfter) && time.Now().After(cert.NotBefore)
|
||||
|
||||
// Build detailed error message if validation failed
|
||||
errorMsg := ""
|
||||
if validationError != nil {
|
||||
errorMsg = validationError.Error()
|
||||
} else if time.Now().After(cert.NotAfter) {
|
||||
errorMsg = "Certificate has expired"
|
||||
} else if time.Now().Before(cert.NotBefore) {
|
||||
errorMsg = "Certificate is not yet valid"
|
||||
}
|
||||
|
||||
// Create comprehensive result
|
||||
result := &types.OperationResult{
|
||||
Type: types.OperationSSL,
|
||||
Host: hostname,
|
||||
Success: isValid,
|
||||
ResponseTime: responseTime,
|
||||
Error: errorMsg,
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
|
||||
// SSL specific fields - using organization instead of full DN
|
||||
SSLValidFrom: cert.NotBefore,
|
||||
SSLValidTill: cert.NotAfter,
|
||||
SSLDaysLeft: daysLeft,
|
||||
SSLIssuer: issuerOrganization, // Now shows "Google Trust Services" instead of full DN
|
||||
SSLSubject: subjectOrganization, // Now shows organization instead of full DN
|
||||
SSLSerialNumber: cert.SerialNumber.String(),
|
||||
SSLAlgorithm: algorithm,
|
||||
SSLSANs: strings.Join(sans, ","),
|
||||
SSLResolvedIP: resolvedIP,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// createErrorResult creates a standardized error result
|
||||
func (op *SSLOperation) createErrorResult(domain string, startTime time.Time, errorMsg string) (*types.OperationResult, error) {
|
||||
return &types.OperationResult{
|
||||
Type: types.OperationSSL,
|
||||
Host: domain,
|
||||
Success: false,
|
||||
ResponseTime: time.Since(startTime),
|
||||
Error: errorMsg,
|
||||
StartTime: startTime,
|
||||
EndTime: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
|
||||
package operations
|
||||
|
||||
import (
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// normalizeDomain cleans and normalizes the domain input
|
||||
func (op *SSLOperation) normalizeDomain(domain string) string {
|
||||
// Remove protocol prefixes
|
||||
domain = strings.Replace(domain, "https://", "", 1)
|
||||
domain = strings.Replace(domain, "http://", "", 1)
|
||||
|
||||
// Remove trailing slash and path
|
||||
if idx := strings.Index(domain, "/"); idx != -1 {
|
||||
domain = domain[:idx]
|
||||
}
|
||||
|
||||
// Trim whitespace
|
||||
domain = strings.TrimSpace(domain)
|
||||
|
||||
return domain
|
||||
}
|
||||
|
||||
// formatDistinguishedName formats the certificate distinguished name
|
||||
func (op *SSLOperation) formatDistinguishedName(name pkix.Name) string {
|
||||
var parts []string
|
||||
|
||||
if name.CommonName != "" {
|
||||
parts = append(parts, fmt.Sprintf("CN=%s", name.CommonName))
|
||||
}
|
||||
|
||||
for _, org := range name.Organization {
|
||||
parts = append(parts, fmt.Sprintf("O=%s", org))
|
||||
}
|
||||
|
||||
for _, orgUnit := range name.OrganizationalUnit {
|
||||
parts = append(parts, fmt.Sprintf("OU=%s", orgUnit))
|
||||
}
|
||||
|
||||
for _, country := range name.Country {
|
||||
parts = append(parts, fmt.Sprintf("C=%s", country))
|
||||
}
|
||||
|
||||
for _, locality := range name.Locality {
|
||||
parts = append(parts, fmt.Sprintf("L=%s", locality))
|
||||
}
|
||||
|
||||
for _, province := range name.Province {
|
||||
parts = append(parts, fmt.Sprintf("ST=%s", province))
|
||||
}
|
||||
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
// extractIssuerOrganization extracts only the organization (O=) from the issuer distinguished name
|
||||
func (op *SSLOperation) extractIssuerOrganization(name pkix.Name) string {
|
||||
// Return the first organization if available
|
||||
if len(name.Organization) > 0 {
|
||||
return name.Organization[0]
|
||||
}
|
||||
|
||||
// Fallback to Common Name if no organization
|
||||
if name.CommonName != "" {
|
||||
return name.CommonName
|
||||
}
|
||||
|
||||
// Last resort fallback
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
// extractSubjectOrganization extracts only the organization (O=) from the subject distinguished name
|
||||
func (op *SSLOperation) extractSubjectOrganization(name pkix.Name) string {
|
||||
// Return the first organization if available
|
||||
if len(name.Organization) > 0 {
|
||||
return name.Organization[0]
|
||||
}
|
||||
|
||||
// Fallback to Common Name if no organization
|
||||
if name.CommonName != "" {
|
||||
return name.CommonName
|
||||
}
|
||||
|
||||
// Last resort fallback
|
||||
return "Unknown"
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package operations
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// validateCertificate performs comprehensive certificate validation
|
||||
func (op *SSLOperation) validateCertificate(cert *x509.Certificate, hostname string) error {
|
||||
now := time.Now()
|
||||
|
||||
// Check if certificate is expired or not yet valid
|
||||
if now.Before(cert.NotBefore) {
|
||||
return fmt.Errorf("certificate is not yet valid (valid from: %v)", cert.NotBefore.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
if now.After(cert.NotAfter) {
|
||||
return fmt.Errorf("certificate has expired (expired on: %v)", cert.NotAfter.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
// Verify hostname matches certificate
|
||||
if err := cert.VerifyHostname(hostname); err != nil {
|
||||
return fmt.Errorf("hostname verification failed: %v", err)
|
||||
}
|
||||
|
||||
// Check key usage - certificates should have digital signature capability
|
||||
if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 {
|
||||
return fmt.Errorf("certificate missing required digital signature key usage")
|
||||
}
|
||||
|
||||
// Check if certificate is self-signed (basic check)
|
||||
if cert.Issuer.CommonName == cert.Subject.CommonName && len(cert.Subject.Organization) == 0 {
|
||||
return fmt.Errorf("certificate appears to be self-signed")
|
||||
}
|
||||
|
||||
// Validate certificate chain if intermediate certificates are present
|
||||
if err := op.validateCertificateChain(cert); err != nil {
|
||||
return fmt.Errorf("certificate chain validation failed: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateCertificateChain performs basic certificate chain validation
|
||||
func (op *SSLOperation) validateCertificateChain(cert *x509.Certificate) error {
|
||||
// Check if the certificate has proper extensions for SSL/TLS
|
||||
hasServerAuth := false
|
||||
for _, usage := range cert.ExtKeyUsage {
|
||||
if usage == x509.ExtKeyUsageServerAuth {
|
||||
hasServerAuth = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasServerAuth {
|
||||
return fmt.Errorf("certificate does not have server authentication extension")
|
||||
}
|
||||
|
||||
// Check certificate version (should be v3 for modern certificates)
|
||||
if cert.Version < 3 {
|
||||
return fmt.Errorf("certificate version %d is outdated (should be v3)", cert.Version)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user