package scan import ( "bytes" "errors" "fmt" "net" "strings" "github.com/cloudflare/cfssl/helpers" "github.com/cloudflare/cfssl/scan/crypto/tls" ) // Sentinel for failures in sayHello. Should always be caught. var errHelloFailed = errors.New("Handshake failed in sayHello") // TLSHandshake contains scanners testing host cipher suite negotiation var TLSHandshake = &Family{ Description: "Scans for host's SSL/TLS version and cipher suite negotiation", Scanners: map[string]*Scanner{ "CipherSuite": { "Determines host's cipher suites accepted and preferred order", cipherSuiteScan, }, "SigAlgs": { "Determines host's accepted signature and hash algorithms", sigAlgsScan, }, "CertsBySigAlgs": { "Determines host's certificate signature algorithm matching client's accepted signature and hash algorithms", certSigAlgsScan, }, "CertsByCiphers": { "Determines host's certificate signature algorithm matching client's accepted ciphers", certSigAlgsScanByCipher, }, "ECCurves": { "Determines the host's ec curve support for TLS 1.2", ecCurveScan, }, }, } func getCipherIndex(ciphers []uint16, serverCipher uint16) (cipherIndex int, err error) { //func getCipherIndex(ciphers []uint16, serverCipher uint16) (cipherIndex int, err error) { // fmt.Println(serverCipher, ciphers) var cipherID uint16 for cipherIndex, cipherID = range ciphers { if serverCipher == cipherID { return } } err = fmt.Errorf("server negotiated ciphersuite we didn't send: %s", tls.CipherSuites[serverCipher]) return } func getCurveIndex(curves []tls.CurveID, serverCurve tls.CurveID) (curveIndex int, err error) { var curveID tls.CurveID for curveIndex, curveID = range curves { if serverCurve == curveID { return } } err = fmt.Errorf("server negotiated elliptic curve we didn't send: %s", tls.Curves[serverCurve]) return } func sayHello(addr, hostname string, ciphers []uint16, curves []tls.CurveID, vers uint16, sigAlgs []tls.SignatureAndHash) (cipherIndex, curveIndex int, certs [][]byte, err error) { tcpConn, err := net.Dial(Network, addr) if err != nil { return } config := defaultTLSConfig(hostname) config.MinVersion = vers config.MaxVersion = vers if ciphers == nil { ciphers = allCiphersIDs() } config.CipherSuites = ciphers if curves == nil { curves = allCurvesIDs() } config.CurvePreferences = curves if sigAlgs == nil { sigAlgs = tls.AllSignatureAndHashAlgorithms } conn := tls.Client(tcpConn, config) serverCipher, serverCurveType, serverCurve, serverVersion, certificates, err := conn.SayHello(sigAlgs) certs = certificates conn.Close() if err != nil { err = errHelloFailed return } if serverVersion != vers { err = fmt.Errorf("server negotiated protocol version we didn't send: %s", tls.Versions[serverVersion]) return } cipherIndex, err = getCipherIndex(ciphers, serverCipher) if tls.CipherSuites[serverCipher].EllipticCurve { if curves == nil { curves = allCurvesIDs() } if serverCurveType != 3 { err = fmt.Errorf("server negotiated non-named ECDH parameters; we didn't analyze them. Server curve type: %d", serverCurveType) return } curveIndex, err = getCurveIndex(curves, serverCurve) } return } func allCiphersIDs() []uint16 { ciphers := make([]uint16, 0, len(tls.CipherSuites)) for cipherID := range tls.CipherSuites { ciphers = append(ciphers, cipherID) } return ciphers } func allECDHECiphersIDs() []uint16 { var ecdheCiphers = map[uint16]tls.CipherSuite{ 0XC006: {Name: "TLS_ECDHE_ECDSA_WITH_NULL_SHA", ForwardSecret: true, EllipticCurve: true}, 0XC007: {Name: "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", ShortName: "ECDHE-ECDSA-RC4-SHA", ForwardSecret: true, EllipticCurve: true}, 0XC008: {Name: "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", ShortName: "ECDHE-ECDSA-DES-CBC3-SHA", ForwardSecret: true, EllipticCurve: true}, 0XC009: {Name: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", ShortName: "ECDHE-ECDSA-AES128-SHA", ForwardSecret: true, EllipticCurve: true}, 0XC00A: {Name: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", ShortName: "ECDHE-ECDSA-AES256-SHA", ForwardSecret: true, EllipticCurve: true}, 0XC010: {Name: "TLS_ECDHE_RSA_WITH_NULL_SHA", ForwardSecret: true, EllipticCurve: true}, 0XC011: {Name: "TLS_ECDHE_RSA_WITH_RC4_128_SHA", ShortName: "ECDHE-RSA-RC4-SHA", ForwardSecret: true, EllipticCurve: true}, 0XC012: {Name: "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", ShortName: "ECDHE-RSA-DES-CBC3-SHA", ForwardSecret: true, EllipticCurve: true}, 0XC013: {Name: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", ShortName: "ECDHE-RSA-AES128-SHA", ForwardSecret: true, EllipticCurve: true}, 0XC014: {Name: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", ShortName: "ECDHE-RSA-AES256-SHA", ForwardSecret: true, EllipticCurve: true}, 0XC023: {Name: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", ShortName: "ECDHE-ECDSA-AES128-SHA256", ForwardSecret: true, EllipticCurve: true}, 0XC024: {Name: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", ShortName: "ECDHE-ECDSA-AES256-SHA384", ForwardSecret: true, EllipticCurve: true}, 0XC027: {Name: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", ShortName: "ECDHE-RSA-AES128-SHA256", ForwardSecret: true, EllipticCurve: true}, 0XC028: {Name: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", ShortName: "ECDHE-RSA-AES256-SHA384", ForwardSecret: true, EllipticCurve: true}, 0XC02B: {Name: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", ShortName: "ECDHE-ECDSA-AES128-GCM-SHA256", ForwardSecret: true, EllipticCurve: true}, 0XC02C: {Name: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", ShortName: "ECDHE-ECDSA-AES256-GCM-SHA384", ForwardSecret: true, EllipticCurve: true}, 0XC02F: {Name: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", ShortName: "ECDHE-RSA-AES128-GCM-SHA256", ForwardSecret: true, EllipticCurve: true}, 0XC030: {Name: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", ShortName: "ECDHE-RSA-AES256-GCM-SHA384", ForwardSecret: true, EllipticCurve: true}, 0XC048: {Name: "TLS_ECDHE_ECDSA_WITH_ARIA_128_CBC_SHA256", ForwardSecret: true, EllipticCurve: true}, 0XC049: {Name: "TLS_ECDHE_ECDSA_WITH_ARIA_256_CBC_SHA384", ForwardSecret: true, EllipticCurve: true}, 0XC04C: {Name: "TLS_ECDHE_RSA_WITH_ARIA_128_CBC_SHA256", ForwardSecret: true, EllipticCurve: true}, 0XC04D: {Name: "TLS_ECDHE_RSA_WITH_ARIA_256_CBC_SHA384", ForwardSecret: true, EllipticCurve: true}, 0XC05D: {Name: "TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384", ForwardSecret: true, EllipticCurve: true}, 0XC060: {Name: "TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256", ForwardSecret: true, EllipticCurve: true}, 0XC061: {Name: "TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384", ForwardSecret: true, EllipticCurve: true}, 0XC072: {Name: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256", ForwardSecret: true, EllipticCurve: true}, 0XC073: {Name: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384", ForwardSecret: true, EllipticCurve: true}, 0XC076: {Name: "TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256", ForwardSecret: true, EllipticCurve: true}, 0XC077: {Name: "TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384", ForwardSecret: true, EllipticCurve: true}, 0XC086: {Name: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_GCM_SHA256", ForwardSecret: true, EllipticCurve: true}, 0XC087: {Name: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_GCM_SHA384", ForwardSecret: true, EllipticCurve: true}, 0XC08A: {Name: "TLS_ECDHE_RSA_WITH_CAMELLIA_128_GCM_SHA256", ForwardSecret: true, EllipticCurve: true}, 0XC08B: {Name: "TLS_ECDHE_RSA_WITH_CAMELLIA_256_GCM_SHA384", ForwardSecret: true, EllipticCurve: true}, 0XC08C: {Name: "TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256", EllipticCurve: true}, 0XC0AC: {Name: "TLS_ECDHE_ECDSA_WITH_AES_128_CCM", ForwardSecret: true, EllipticCurve: true}, 0XC0AD: {Name: "TLS_ECDHE_ECDSA_WITH_AES_256_CCM", ForwardSecret: true, EllipticCurve: true}, 0XC0AE: {Name: "TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8", ForwardSecret: true, EllipticCurve: true}, 0XC0AF: {Name: "TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8", ForwardSecret: true, EllipticCurve: true}, // Non-IANA standardized cipher suites: // ChaCha20, Poly1305 cipher suites are defined in // https://tools.ietf.org/html/draft-agl-tls-chacha20poly1305-04 0XCC13: {Name: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", ForwardSecret: true, EllipticCurve: true}, 0XCC14: {Name: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", ForwardSecret: true, EllipticCurve: true}, } ciphers := make([]uint16, 0, len(ecdheCiphers)) for cipherID := range ecdheCiphers { ciphers = append(ciphers, cipherID) } return ciphers } func allCurvesIDs() []tls.CurveID { curves := make([]tls.CurveID, 0, len(tls.Curves)) for curveID := range tls.Curves { // No unassigned or explicit curves in the scan, per http://tools.ietf.org/html/rfc4492#section-5.4 if curveID == 0 || curveID == 65281 || curveID == 65282 { continue } else { curves = append(curves, curveID) } } return curves } type cipherDatum struct { versionID uint16 curves []tls.CurveID } // cipherVersions contains lists of host's supported cipher suites based on SSL/TLS Version. // If a cipher suite uses ECC, also contains a list of supported curves by SSL/TLS Version. type cipherVersions struct { cipherID uint16 data []cipherDatum } type cipherVersionList []cipherVersions func (cvList cipherVersionList) String() string { cvStrings := make([]string, len(cvList)) for i, c := range cvList { versStrings := make([]string, len(c.data)) for j, d := range c.data { curveStrings := make([]string, len(d.curves)) for k, c := range d.curves { curveStrings[k] = tls.Curves[c] } versStrings[j] = fmt.Sprintf("%s: [ %s ]", tls.Versions[d.versionID], strings.Join(curveStrings, ",")) } cvStrings[i] = fmt.Sprintf("%s\t%s", tls.CipherSuites[c.cipherID], strings.Join(versStrings, ",")) } return strings.Join(cvStrings, "\n") } func (cvList cipherVersionList) MarshalJSON() ([]byte, error) { b := new(bytes.Buffer) cvStrs := make([]string, len(cvList)) for i, cv := range cvList { versStrings := make([]string, len(cv.data)) for j, d := range cv.data { curveStrings := make([]string, len(d.curves)) if len(d.curves) > 0 { for k, c := range d.curves { curveStrings[k] = fmt.Sprintf("\"%s\"", tls.Curves[c]) } versStrings[j] = fmt.Sprintf("{\"%s\":[%s]}", tls.Versions[d.versionID], strings.Join(curveStrings, ",")) } else { versStrings[j] = fmt.Sprintf("\"%s\"", tls.Versions[d.versionID]) } } cvStrs[i] = fmt.Sprintf("{\"%s\":[%s]}", tls.CipherSuites[cv.cipherID].String(), strings.Join(versStrings, ",")) } fmt.Fprintf(b, "[%s]", strings.Join(cvStrs, ",")) return b.Bytes(), nil } func doCurveScan(addr, hostname string, vers, cipherID uint16, ciphers []uint16) (supportedCurves []tls.CurveID, err error) { allCurves := allCurvesIDs() curves := make([]tls.CurveID, len(allCurves)) copy(curves, allCurves) for len(curves) > 0 { var curveIndex int _, curveIndex, _, err = sayHello(addr, hostname, []uint16{cipherID}, curves, vers, nil) if err != nil { // This case is expected, because eventually we ask only for curves the server doesn't support if err == errHelloFailed { err = nil break } return } curveID := curves[curveIndex] supportedCurves = append(supportedCurves, curveID) curves = append(curves[:curveIndex], curves[curveIndex+1:]...) } return } // cipherSuiteScan returns, by TLS Version, the sort list of cipher suites // supported by the host func cipherSuiteScan(addr, hostname string) (grade Grade, output Output, err error) { var cvList cipherVersionList allCiphers := allCiphersIDs() var vers uint16 for vers = tls.VersionTLS12; vers >= tls.VersionSSL30; vers-- { ciphers := make([]uint16, len(allCiphers)) copy(ciphers, allCiphers) for len(ciphers) > 0 { var cipherIndex int cipherIndex, _, _, err = sayHello(addr, hostname, ciphers, nil, vers, nil) if err != nil { if err == errHelloFailed { err = nil break } return } if vers == tls.VersionSSL30 { grade = Warning } cipherID := ciphers[cipherIndex] // If this is an EC cipher suite, do a second scan for curve support var supportedCurves []tls.CurveID if tls.CipherSuites[cipherID].EllipticCurve { supportedCurves, err = doCurveScan(addr, hostname, vers, cipherID, ciphers) if len(supportedCurves) == 0 { err = errors.New("couldn't negotiate any curves") } } for i, c := range cvList { if cipherID == c.cipherID { cvList[i].data = append(c.data, cipherDatum{vers, supportedCurves}) goto exists } } cvList = append(cvList, cipherVersions{cipherID, []cipherDatum{{vers, supportedCurves}}}) exists: ciphers = append(ciphers[:cipherIndex], ciphers[cipherIndex+1:]...) } } if len(cvList) == 0 { err = errors.New("couldn't negotiate any cipher suites") return } if grade != Warning { grade = Good } output = cvList return } // sigAlgsScan returns the accepted signature and hash algorithms of the host func sigAlgsScan(addr, hostname string) (grade Grade, output Output, err error) { var supportedSigAlgs []tls.SignatureAndHash for _, sigAlg := range tls.AllSignatureAndHashAlgorithms { _, _, _, e := sayHello(addr, hostname, nil, nil, tls.VersionTLS12, []tls.SignatureAndHash{sigAlg}) if e == nil { supportedSigAlgs = append(supportedSigAlgs, sigAlg) } } if len(supportedSigAlgs) > 0 { grade = Good output = supportedSigAlgs } else { err = errors.New("no SigAlgs supported") } return } // certSigAlgScan returns the server certificate with various sigature and hash algorithms in the ClientHello func certSigAlgsScan(addr, hostname string) (grade Grade, output Output, err error) { var certSigAlgs = make(map[string]string) for _, sigAlg := range tls.AllSignatureAndHashAlgorithms { _, _, derCerts, e := sayHello(addr, hostname, nil, nil, tls.VersionTLS12, []tls.SignatureAndHash{sigAlg}) if e == nil { if len(derCerts) == 0 { return Bad, nil, errors.New("no certs returned") } certs, _, err := helpers.ParseCertificatesDER(derCerts[0], "") if err != nil { return Bad, nil, err } certSigAlgs[sigAlg.String()] = helpers.SignatureString(certs[0].SignatureAlgorithm) //certSigAlgs = append(certSigAlgs, certs[0].SignatureAlgorithm) } } if len(certSigAlgs) > 0 { grade = Good output = certSigAlgs } else { err = errors.New("no SigAlgs supported") } return } // certSigAlgScan returns the server certificate with various ciphers in the ClientHello func certSigAlgsScanByCipher(addr, hostname string) (grade Grade, output Output, err error) { var certSigAlgs = make(map[string]string) for cipherID := range tls.CipherSuites { _, _, derCerts, e := sayHello(addr, hostname, []uint16{cipherID}, nil, tls.VersionTLS12, []tls.SignatureAndHash{}) if e == nil { if len(derCerts) == 0 { return Bad, nil, errors.New("no certs returned") } certs, _, err := helpers.ParseCertificatesDER(derCerts[0], "") if err != nil { return Bad, nil, err } certSigAlgs[tls.CipherSuites[cipherID].Name] = helpers.SignatureString(certs[0].SignatureAlgorithm) //certSigAlgs = append(certSigAlgs, certs[0].SignatureAlgorithm) } } if len(certSigAlgs) > 0 { grade = Good output = certSigAlgs } else { err = errors.New("no cipher supported") } return } // ecCurveScan returns the elliptic curves supported by the host. func ecCurveScan(addr, hostname string) (grade Grade, output Output, err error) { allCurves := allCurvesIDs() curves := make([]tls.CurveID, len(allCurves)) copy(curves, allCurves) var supportedCurves []string for len(curves) > 0 { var curveIndex int _, curveIndex, _, err = sayHello(addr, hostname, allECDHECiphersIDs(), curves, tls.VersionTLS12, nil) if err != nil { // This case is expected, because eventually we ask only for curves the server doesn't support if err == errHelloFailed { err = nil break } return } curveID := curves[curveIndex] supportedCurves = append(supportedCurves, tls.Curves[curveID]) curves = append(curves[:curveIndex], curves[curveIndex+1:]...) } output = supportedCurves grade = Good return }