package ssl import ( "crypto/tls" "crypto/x509" "fmt" "net" "time" "arcline-audit/internal/types" ) // insecureCipherSuites is a set of cipher suite IDs considered weak. var insecureCipherSuites = map[uint16]bool{ tls.TLS_RSA_WITH_RC4_128_SHA: true, tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA: true, tls.TLS_RSA_WITH_AES_128_CBC_SHA: true, tls.TLS_RSA_WITH_AES_256_CBC_SHA: true, tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA: true, tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA: true, tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA: true, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA: true, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA: true, tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA: true, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA: true, tls.TLS_RSA_WITH_AES_128_GCM_SHA256: true, tls.TLS_RSA_WITH_AES_256_GCM_SHA384: true, } // Run performs all SSL/TLS checks for the given domain. func Run(domain string) types.SSLResult { addr := net.JoinHostPort(domain, "443") dialer := &net.Dialer{Timeout: 10 * time.Second} conn, err := tls.DialWithDialer(dialer, "tcp", addr, &tls.Config{ InsecureSkipVerify: true, }) if err != nil { return types.SSLResult{ Checks: []types.CheckResult{ {Status: types.StatusFail, Message: fmt.Sprintf("failed to connect: %v", err)}, }, } } defer conn.Close() state := conn.ConnectionState() var checks []types.CheckResult // Certificate validity and details checks = append(checks, checkCertificates(state.PeerCertificates)...) // TLS version checks = append(checks, checkTLSVersion(state.Version)...) // Cipher suite checks = append(checks, checkCipherSuite(state.CipherSuite)...) result := types.SSLResult{Checks: checks} if len(state.PeerCertificates) > 0 { cert := state.PeerCertificates[0] result.Issuer = cert.Issuer.String() result.Expiry = cert.NotAfter result.DaysLeft = int(time.Until(cert.NotAfter).Hours() / 24) } result.TLSVersion = tlsVersionName(state.Version) return result } func checkCertificates(certs []*x509.Certificate) []types.CheckResult { var checks []types.CheckResult if len(certs) == 0 { checks = append(checks, types.CheckResult{ Status: types.StatusFail, Message: "no certificates presented", }) return checks } leaf := certs[0] now := time.Now() // Check expiry if now.After(leaf.NotAfter) { checks = append(checks, types.CheckResult{ Status: types.StatusFail, Message: fmt.Sprintf("certificate expired on %s", leaf.NotAfter.Format("2006-01-02")), }) } else { daysLeft := int(time.Until(leaf.NotAfter).Hours() / 24) if daysLeft < 30 { checks = append(checks, types.CheckResult{ Status: types.StatusWarn, Message: fmt.Sprintf("certificate expires in %d days (%s)", daysLeft, leaf.NotAfter.Format("2006-01-02")), }) } else { checks = append(checks, types.CheckResult{ Status: types.StatusOK, Message: fmt.Sprintf("valid certificate (%s)", leaf.NotAfter.Format("2006-01-02")), }) } } // Check self-signed if leaf.Issuer.String() == leaf.Subject.String() { checks = append(checks, types.CheckResult{ Status: types.StatusWarn, Message: "self-signed certificate", }) } else { checks = append(checks, types.CheckResult{ Status: types.StatusOK, Message: "not self-signed", }) } // Check chain completeness if len(certs) >= 2 { checks = append(checks, types.CheckResult{ Status: types.StatusOK, Message: "certificate chain is complete", }) } else { checks = append(checks, types.CheckResult{ Status: types.StatusWarn, Message: "certificate chain may be incomplete", }) } return checks } func checkTLSVersion(vers uint16) []types.CheckResult { name := tlsVersionName(vers) switch vers { case tls.VersionTLS13: return []types.CheckResult{{Status: types.StatusOK, Message: fmt.Sprintf("TLS %s", name)}} case tls.VersionTLS12: return []types.CheckResult{{Status: types.StatusOK, Message: fmt.Sprintf("TLS %s", name)}} case tls.VersionTLS11, tls.VersionTLS10: return []types.CheckResult{{Status: types.StatusWarn, Message: fmt.Sprintf("TLS %s is insecure", name)}} case 0: return []types.CheckResult{{Status: types.StatusFail, Message: "unknown TLS version"}} default: return []types.CheckResult{{Status: types.StatusInfo, Message: fmt.Sprintf("TLS %s", name)}} } } func checkCipherSuite(id uint16) []types.CheckResult { name := tls.CipherSuiteName(id) if name == "" { name = fmt.Sprintf("unknown (0x%04X)", id) } if insecureCipherSuites[id] { return []types.CheckResult{{Status: types.StatusWarn, Message: fmt.Sprintf("weak cipher suite: %s", name)}} } return []types.CheckResult{{Status: types.StatusOK, Message: fmt.Sprintf("cipher suite: %s", name)}} } func tlsVersionName(v uint16) string { switch v { case tls.VersionTLS10: return "1.0" case tls.VersionTLS11: return "1.1" case tls.VersionTLS12: return "1.2" case tls.VersionTLS13: return "1.3" default: return fmt.Sprintf("0x%04X", v) } }