diff --git a/library/network/http_client/src/http_client_secure_config.e b/library/network/http_client/src/http_client_secure_config.e new file mode 100644 index 00000000..a244671e --- /dev/null +++ b/library/network/http_client/src/http_client_secure_config.e @@ -0,0 +1,177 @@ +note + description: "Summary description for {HTTP_CLIENT_SECURE_CONFIG}." + date: "$Date$" + revision: "$Revision$" + +class + HTTP_CLIENT_SECURE_CONFIG + +feature -- Access + + verify_peer: BOOLEAN + -- Verify the peer's SSL certificate. + -- If the verification fails to prove that the certificate is authentic, the connection fails. + -- by default is disabled. call enable_verify_peer. + + verify_host: BOOLEAN + -- Verify the certificate's name against host. + -- Checks the server's certificate's claimed identity. + -- by default is disabled. call enable_verify_host. + + certificate_type: detachable STRING + -- specify type of the client SSL certificate. + -- PEM, DER, P12 + --| LibCurl: Supported formats are "PEM" and "DER", except with Secure Transport. + --| OpenSSL (versions 0.9.3 and later) and Secure Transport (on iOS 5 or later, or OS X 10.7 or later) also support "P12" for PKCS#12-encoded files. + --| https://curl.haxx.se/libcurl/c/CURLOPT_SSLCERTTYPE.html + + client_certificate: detachable STRING_32 + -- specify the certificate for client authentication. + + passphrase: detachable STRING_32 + -- password required for a private key. + + certificate_authority: detachable STRING_32 + -- path to Certificate Authority (CA) bundle. + + tls_version: INTEGER + -- TLS version 1.2 or 1.3 + +feature -- Change Element + + enable_verify_host + --Enable verify the certificate's name against host. + do + verify_host := True + ensure + verify_host_set: verify_host = True + end + + enable_verify_peer + -- Enable verify the certificate's name against host. + do + verify_peer := True + ensure + verify_peer_set: verify_peer = True + end + + set_certificate_type (a_cert: STRING) + -- Set type of the client SSL certificate (PEM, P12) `certificate_type` with `a_cert`. + --| TODO add a precondition to verify the type of the certificate. + note + eis:"name=certificates formats", "src=https://www.ssls.com/knowledgebase/what-are-certificate-formats-and-what-is-the-difference-between-them/", "protocol=uri" + require + is_valid_certificate_type: certificates_formats.has (a_cert.as_lower) + do + certificate_type := a_cert + ensure + certificate_type_set: certificate_type = a_cert + end + + set_client_certificate (a_client_certificate: STRING_32) + -- Set path to client certificate `client_certificate` with `a_client_certificate` for authentication. + do + client_certificate := a_client_certificate + ensure + client_certificate_set: client_certificate = a_client_certificate + end + + set_passphrase (a_password: STRING_32) + -- Set the passphrase `passphrase` with `a_password`, if the private key required it. + do + passphrase := a_password + ensure + passphrase_set: passphrase = a_password + end + + set_certificate_authority (a_certificate: STRING_32) + -- set path to Certificate Authority `certificate_authority` with `a_certificate`. + do + certificate_authority := a_certificate + ensure + certificate_authority_set: certificate_authority = a_certificate + end + + set_tls_version (a_version: INTEGER) + -- set preferred TLS/SSL version `tls_version` with `a_version` + require + valid_tls_version: is_valid_tls_verion (a_version) + do + tls_version := a_version + ensure + tls_version_set: tls_version = a_version + end + +feature -- Certificates Types + + certificates_formats: SET [STRING] + note + eis:"name=certificates formats", "src=https://www.ssls.com/knowledgebase/what-are-certificate-formats-and-what-is-the-difference-between-them/", "protocol=uri" + do + create {ARRAYED_SET [STRING]} Result.make (9) + -- Base64 + -- PEM + Result.put ("pem") + Result.put ("crt") + Result.put ("ca-bundle") + --PKCS#7 + Result.put ("p7b") + Result.put ("p7s") + -- Binary + -- DER + Result.put ("der") + Result.put ("cer") + --PKCS#12 + Result.put ("pfx") + Result.put ("p12") + Result.compare_objects + ensure + is_class: class + end + +feature -- TLS versions + + tls_1_2: INTEGER = 2 + + tls_1_3: INTEGER = 3 + + is_valid_tls_verion (a_version: INTEGER): BOOLEAN + -- Is `a_version` a valid and supported TLS version? + do + Result := a_version = tls_1_2 or a_version = tls_1_3 + end + + is_tls_1_2: BOOLEAN + do + Result := tls_version = tls_1_2 + end + + is_tls_1_3: BOOLEAN + do + Result := tls_version = tls_1_3 + end + +feature -- Reset + + reset + -- Clear config. + do + verify_host := False + verify_peer := False + certificate_type := Void + client_certificate := Void + passphrase := Void + end + +note + copyright: "2011-2019, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" + source: "[ + Eiffel Software + 5949 Hollister Ave., Goleta, CA 93117 USA + Telephone 805-685-1006, Fax 805-685-6869 + Website http://www.eiffel.com + Customer support http://support.eiffel.com + ]" + +end diff --git a/library/network/http_client/src/http_client_session.e b/library/network/http_client/src/http_client_session.e index 3968c7ec..53e3af2c 100644 --- a/library/network/http_client/src/http_client_session.e +++ b/library/network/http_client/src/http_client_session.e @@ -270,6 +270,10 @@ feature -- Settings proxy: detachable TUPLE [host: READABLE_STRING_8; port: INTEGER] -- Proxy information [`host' and `port'] + secure_config: detachable HTTP_CLIENT_SECURE_CONFIG + -- http client secure configuration. + --| HTTPS usage with client certificates. + feature -- Access base_url: READABLE_STRING_8 @@ -445,8 +449,17 @@ feature -- Element change ciphers_setting_set: attached ciphers_setting as c_setting and then c_setting.same_string (a_ciphers_setting) end + + set_secure_config (a_config: like secure_config) + -- Set 'secure_config' with 'a_config'. + do + secure_config := a_config + ensure + secure_config_set: secure_config = a_config + end + note - copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + copyright: "2011-2019, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" source: "[ Eiffel Software diff --git a/library/network/http_client/src/spec/libcurl/libcurl_default_function.e b/library/network/http_client/src/spec/libcurl/libcurl_default_function.e index 33200efa..de42c696 100644 --- a/library/network/http_client/src/spec/libcurl/libcurl_default_function.e +++ b/library/network/http_client/src/spec/libcurl/libcurl_default_function.e @@ -5,8 +5,8 @@ note ]" status: "See notice at end of class." legal: "See notice at end of class." - date: "$Date: 2009-04-09 20:51:20 +0200 (Thu, 09 Apr 2009) $" - revision: "$Revision: 78146 $" + date: "$Date$" + revision: "$Revision$" class LIBCURL_DEFAULT_FUNCTION diff --git a/library/network/http_client/src/spec/libcurl/libcurl_http_client_request.e b/library/network/http_client/src/spec/libcurl/libcurl_http_client_request.e index cc053023..4d06d482 100644 --- a/library/network/http_client/src/spec/libcurl/libcurl_http_client_request.e +++ b/library/network/http_client/src/spec/libcurl/libcurl_http_client_request.e @@ -82,6 +82,11 @@ feature -- Execution --| Configure cURL session initialize_curl_session (ctx, curl, curl_easy, curl_handle) + --| Condigure cURL secure session + if attached {HTTP_CLIENT_SECURE_CONFIG} session.secure_config as l_config then + initialize_curl_security_session (curl_easy, curl_handle, l_config) + end + --| URL l_url := url @@ -300,7 +305,11 @@ feature -- Execution end end else - Result.set_error_message ("Error: cURL Error[" + l_result.out + "]") + if attached curl.error_message (l_result) as err_msg then + Result.set_error_message ("Error: " + {UTF_CONVERTER}.utf_32_string_to_utf_8_string_8 (err_msg)) + else + Result.set_error_message ("Error: cURL Error[" + l_result.out + "]") + end Result.status := response_status_code (curl_easy, curl_handle) end @@ -400,6 +409,56 @@ feature -- Execution end end + + initialize_curl_security_session (curl_easy: CURL_EASY_EXTERNALS; curl_handle: POINTER; a_config: HTTP_CLIENT_SECURE_CONFIG) + do + --| TLS version. + if a_config.is_valid_tls_verion (a_config.tls_version) then + if a_config.is_tls_1_2 then + -- ask libcurl to use TLS version 1.2 or later */ + curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.CURLOPT_SSLVERSION, {CURL_OPT_CONSTANTS}.CURL_SSLVERSION_TLSv1_2) + end + if a_config.is_tls_1_3 then + -- ask libcurl to use TLS version 1.3 or later */ + curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.CURLOPT_SSLVERSION, {CURL_OPT_CONSTANTS}.CURL_SSLVERSION_TLSv1_3) + end + end + + --| Cert Type + if attached a_config.certificate_type as cert_type then + -- Format P12, PEM + curl_easy.setopt_string (curl_handle, {CURL_OPT_CONSTANTS}.CURLOPT_SSLCERTTYPE, cert_type) + end + + --| set the passphrase (if the key has one...) + if attached a_config.passphrase as passphrase then + curl_easy.setopt_string (curl_handle, {CURL_OPT_CONSTANTS}.CURLOPT_KEYPASSWD, passphrase) + end + + --| set the cert for client authentication + if attached a_config.client_certificate as client_certificate then + curl_easy.setopt_string (curl_handle, {CURL_OPT_CONSTANTS}.CURLOPT_SSLCERT, client_certificate) + end + + --| set the file with the certs vaildating the server + if attached a_config.certificate_authority as certificate_authority then + curl_easy.setopt_string (curl_handle, {CURL_OPT_CONSTANTS}.CURLOPT_CAINFO, certificate_authority) + end + + --| Verify the peer's SSL certificate + if a_config.verify_peer then + -- if the verification fails to prove that the certificate is authentic, the connection fails. + curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.CURLOPT_SSL_VERIFYPEER, 1) + end + + --| Verify the certificate's name against host + if a_config.verify_host then + -- checking the server's certificate's claimed identity. + curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.CURLOPT_SSL_VERIFYHOST, 2) + end + + end + feature {NONE} -- Implementation response_status_code (curl_easy: CURL_EASY_EXTERNALS; curl_handle: POINTER): INTEGER @@ -452,7 +511,7 @@ feature {NONE} -- Implementation end note - copyright: "2011-2018, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + copyright: "2011-2019, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" source: "[ Eiffel Software diff --git a/library/network/http_client/tests/client.p12 b/library/network/http_client/tests/client.p12 new file mode 100644 index 00000000..f021abaa Binary files /dev/null and b/library/network/http_client/tests/client.p12 differ diff --git a/library/network/http_client/tests/client.pem b/library/network/http_client/tests/client.pem new file mode 100644 index 00000000..b71c24e1 --- /dev/null +++ b/library/network/http_client/tests/client.pem @@ -0,0 +1,64 @@ +Bag Attributes + localKeyID: 28 D8 AB 79 A7 97 6F EE D3 6D 1D F0 B9 AC 3D 3F 36 B7 C4 DF +subject=/C=US/ST=California/L=San Francisco/O=BadSSL/CN=BadSSL Client Certificate +issuer=/C=US/ST=California/L=San Francisco/O=BadSSL/CN=BadSSL Client Root Certificate Authority +-----BEGIN CERTIFICATE----- +MIIEnTCCAoWgAwIBAgIJAPC7KMFjfslXMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp +c2NvMQ8wDQYDVQQKDAZCYWRTU0wxMTAvBgNVBAMMKEJhZFNTTCBDbGllbnQgUm9v +dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMTcxMTE2MDUzNjMzWhcNMTkxMTE2 +MDUzNjMzWjBvMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQG +A1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0GA1UECgwGQmFkU1NMMSIwIAYDVQQDDBlC +YWRTU0wgQ2xpZW50IENlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxzdfEeseTs/rukjly6MSLHM+Rh0enA3Ai4Mj2sdl31x3SbPoen08 +utVhjPmlxIUdkiMG4+ffe7N+JtDLG75CaxZp9CxytX7kywooRBJsRnQhmQPca8MR +WAJBIz+w/L+3AFkTIqWBfyT+1VO8TVKPkEpGdLDovZOmzZAASi9/sj+j6gM7AaCi +DeZTf2ES66abA5pOp60Q6OEdwg/vCUJfarhKDpi9tj3P6qToy9Y4DiBUhOct4MG8 +w5XwmKAC+Vfm8tb7tMiUoU0yvKKOcL6YXBXxB2kPcOYxYNobXavfVBEdwSrjQ7i/ +s3o6hkGQlm9F7JPEuVgbl/Jdwa64OYIqjQIDAQABoy0wKzAJBgNVHRMEAjAAMBEG +CWCGSAGG+EIBAQQEAwIHgDALBgNVHQ8EBAMCBeAwDQYJKoZIhvcNAQELBQADggIB +AKpzk1ZTunWuof3DIer2Abq7IV3STGeFaoH4TuHdSbmXwC0KuPkv7wVPgPekyRaH +b9CBnsreRF7eleD1M63kakhdnA1XIbdJw8sfSDlKdI4emmb4fzdaaPxbrkQ5IxOB +QDw5rTUFVPPqFWw1bGP2zrKD1/i1pxUtGM0xem1jR7UZYpsSPs0JCOHKZOmk8OEW +Uy+Jp4gRzbMLZ0TrvajGEZXRepjOkXObR81xZGtvTNP2wl1zm13ffwIYdqJUrf1H +H4miU9lVX+3/Z+2mVHBWhzBgbTmo06s3uwUE6JsxUGm2/w4NNblRit0uQcGw7ba8 +kl2d5rZQscFsqNFz2vRjj1G0dO8S3owmuF0izZO9Fqvq0jB6oaUkxcAcTKFSjs2z +wy1oy+cu8iO3GRbfAW7U0xzGp9MnkdPS5dHzvhod3/DK0YVskfxZF7M8GhkjT7Qm +2EUBQNNMNXC3g/GXTdXOgqqjW5GXahI8Z6Q4OYN6xZwuEhizwKkgojwaww2YgYT9 +MJXciJZWr3QXvFdBH7m0zwpKgQ1wm6j3yeyuRphq2lEtU3OQl55A3tXtvqyMXsxk +xMCCNQdmKQt0WYmMS3Xj/AfAY2sjCWziDflvW5mGCUjSYdZ+r3JIIF4m/FNCIO1d +Ioacp9qb0qL9duFlVHtFiPgoKrEdJaNVUL7NG9ppF8pR +-----END CERTIFICATE----- +Bag Attributes + localKeyID: 28 D8 AB 79 A7 97 6F EE D3 6D 1D F0 B9 AC 3D 3F 36 B7 C4 DF +Key Attributes: +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,8906E5B87ECB8682 + +cY/EOwDILOiCPzyqZanlrhrb5h+fCnS/mHhsNme0Zyrk9udaBZKwxkPtUC6rJZ9/ +rX4HQch9tuVy992CDe1bl4l+3EfqshUhxtivG8GHiFj7TNo5Ia9LaE+KwugB2rdX +J8odwWmwE8Y9TX09WyPKHikjg2nAOgN3Gf+GeJW6fL9xcEVmIgDyX6kBusQYWIQ3 +PMYqyaBhL8UCZVRpiUINTrJXCnNAI4t6K0i4J+hjyekRnA5PQ+A/0+C9J+kQW+wj +uDWkQtyyBNsscGhXrL9ita7/UtJa+/aS99FLTnxD+fqyU4svzNIQWmZBcIxb9Lxq +LHcxNaQCtFycD1iU8Tsq9HY1apKrdPBjaaYne6qCg8nIPWpo3gWoffu+R3EfVcdZ +lNgKL3Cd1oCJy/Vcz+u0iayCPT0zfP1dLlGpL2U7L7+8pJ88FbXt/F1th9QEUp5z +dISg/6ukHyIkfXwoxYXkin9fW2clo99+fuXkJaVPEOnRmr0+kf+1S0Uw8fkete4v +f0IUxUwydxCXFN2ZFZ6o/LFLMLRfNAqGSGjqGeY1L0ILkJPo3KBqlmUhl2FIROOW +4rSrfpujjsOqlPncJ9apW04RnqhY7t0YOtp0rMK+gIDteKD+utCyh+UAetiiqTiB +ZDup/kzPNDClSDU6cgRbZUf/Nt8RiBcVeX4TfDb1eEhqV0BTHrLOBo8yp6ETDlJM +CKO5i9kb5fUaFPFD5VTc5rnY4qV/hUj/uyAVK757A+m9nn7fYOMeaRAgOEvChYSf +Qam0VPgpJxfjsaw0BFOMyfsHNJqvTbPXA+hIKD3fhP6jNcgpKTLHJp89J4XUOeIR +tvh/u7vljhVygP7ZQOpCoX1xSMdJMO7Qhrh3O/2fq/EJertJD2PQ0Zgqb8wosHn/ +Aw9VWT6849jL55Xd5l3zXmI9vU0Le4HP3NstV9jjpcp91dU9yfQpcuIo8U26u+4r +LR1VmhZJjo7FBOpyJZ1Jb3vyp+nPI+tH214DhM9LuXvMbf3ORhXTMOlqSABUmo/+ +t+QjVfcEuhFR9CTVWZUIXflJk/euvzqTQdm8iz7JuFzQOhoXjiPIq0GqtCk10SeL +zqHz1s0TZNcrZyzkmiHuWjGVwHN/XZA3dW67uj522hD7EzucKE9CoCdJ3f4JEWmS +CQwEbba6SKAR6iBlouIYLVdkOBgimGiF13rCwvdN234hQeJT8Wc87iG+uDw73PJL ++amDglATH4wIpBk4xmjh/GTRDK0yH9jp7Dv9iwShbjk0yOuoz5yDn8VPqRIREn8d +9sAaiFUUQ/9XrdlA5F+49OznClDWLKHK8sSAAFyrzvoCcqseSKbvLyrlHGT0fiot +obgDu/W+K2xEOjQeaIyVI5J1qOi6k78fyv1vutjEs6mcTRtDAIxi+V5y5lXUEj0v +OWYsbp9yb8Yq602vV8UYSROd+1xdE+7Td3ENLYE7MnVqju7a5NRfnZYgAU03NgIf +nHGFZC6/tMz/PXS+D0dqzXxwEjH5JQzGBjvSQHK09gHtCfcyshMQQWtXZGQViZX8 +QdYXiaq67nJex0DjWTt56a4EgsdYC1J28bJ3GAkrWNkDFRmlx49zvA== +-----END RSA PRIVATE KEY----- diff --git a/library/network/http_client/tests/test_http_client_i.e b/library/network/http_client/tests/test_http_client_i.e index 65d5105e..621ec4d4 100644 --- a/library/network/http_client/tests/test_http_client_i.e +++ b/library/network/http_client/tests/test_http_client_i.e @@ -74,6 +74,89 @@ feature -- Test routines end end + test_http_client_ssl_with_valid_certs + -- New test routine + local + sess: like new_session + h: STRING_8 + config: HTTP_CLIENT_SECURE_CONFIG + do + --| Set secure configuration + create config + config.set_tls_version ({HTTP_CLIENT_SECURE_CONFIG}.tls_1_2) + config.set_certificate_type ("PEM") + config.set_client_certificate ("./client.pem") + config.set_passphrase ("badssl.com") + config.enable_verify_host + config.enable_verify_peer + + + sess := new_session ("https://client.badssl.com") + sess.set_secure_config (config) + + if attached sess.get ("/", Void) as res then + assert ("Get returned without error", not res.error_occurred) + create h.make_empty + if attached res.headers as hds then + across + hds as c + loop + h.append (c.item.name + ": " + c.item.value + "%R%N") + end + end + assert ("200 ok", res.status = 200) + end + end + + test_http_client_ssl_with_certs_missing_passphrase + -- New test routine + local + sess: like new_session + h: STRING_8 + config: HTTP_CLIENT_SECURE_CONFIG + do + --| Set secure configuration + create config + config.set_tls_version ({HTTP_CLIENT_SECURE_CONFIG}.tls_1_2) + config.set_certificate_type ({STRING_8}"PEM") + config.set_client_certificate ("./client.pem") + config.enable_verify_host + config.enable_verify_peer + + + sess := new_session ("https://client.badssl.com") + sess.set_secure_config (config) + + if attached sess.get ("/", Void) as res then + assert ("Get returned without error", res.error_occurred) + end + end + + + test_http_client_ssl_with_missing_certs + -- New test routine + local + sess: like new_session + h: STRING_8 + do + + sess := new_session ("https://client.badssl.com") + + if attached sess.get ("/", Void) as res then + assert ("Get returned without error", not res.error_occurred) + create h.make_empty + if attached res.headers as hds then + across + hds as c + loop + h.append (c.item.name + ": " + c.item.value + "%R%N") + end + end + assert ("400", res.status = 400) + end + end + + test_abs_url local sess: like new_session diff --git a/library/network/http_client/tests/test_libcurl_http_client.e b/library/network/http_client/tests/test_libcurl_http_client.e index ff35a09d..9b02f781 100644 --- a/library/network/http_client/tests/test_libcurl_http_client.e +++ b/library/network/http_client/tests/test_libcurl_http_client.e @@ -32,6 +32,24 @@ feature -- Tests test_http_client_ssl end + test_libcurl_http_client_ssl_with_valid_certs + do + test_http_client_ssl_with_valid_certs + end + + test_libcurl_http_client_ssl_with_missing_certs + do + test_http_client_ssl_with_missing_certs + end + + test_libcurl_http_client_ssl_with_certs_missing_passphrase + do + test_http_client_ssl_with_certs_missing_passphrase + end + + + + test_libcurl_abs_url do test_abs_url