From 5b854442448af96d57135ba7328b0c21f1f80f40 Mon Sep 17 00:00:00 2001 From: Federico Ceratto Date: Fri, 20 Mar 2020 16:11:39 +0000 Subject: [PATCH] SSL certificate verify GitHub action (#13697) * Implement SSL/TLS certificate checking #782 * SSL: Add nimDisableCertificateValidation Remove NIM_SSL_CERT_VALIDATION env var tests/untestable/thttpclient_ssl.nim ran successfully on Linux with libssl 1.1.1d * SSL: update integ test to skip flapping tests * Revert .travis.yml change * nimDisableCertificateValidation disable imports Prevent loading symbols that are not defined on older SSL libs * SSL: disable verification in net.nim ..when nimDisableCertificateValidation is set * Update changelog * Fix peername type * Add define check for windows * Disable test on windows * Add exprimental GitHub action CI for SSL * Test nimDisableCertificateValidation --- .github/workflows/ci_ssl.yml | 91 ++++++++ appveyor.yml | 2 + changelog.md | 2 + lib/pure/httpclient.nim | 9 + lib/pure/net.nim | 88 ++++++-- lib/pure/ssl_certs.nim | 97 +++++++++ lib/wrappers/openssl.nim | 71 ++++++ tests/stdlib/thttpclient_ssl.nim | 126 +++++++++++ tests/stdlib/thttpclient_ssl_cert.pem | 29 +++ tests/stdlib/thttpclient_ssl_key.pem | 52 +++++ tests/untestable/thttpclient_ssl.nim | 203 ++++++++++++++++++ tests/untestable/thttpclient_ssl_disabled.nim | 40 ++++ tests/untestable/thttpclient_ssl_env_var.nim | 74 +++++++ 13 files changed, 872 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/ci_ssl.yml create mode 100644 lib/pure/ssl_certs.nim create mode 100644 tests/stdlib/thttpclient_ssl.nim create mode 100644 tests/stdlib/thttpclient_ssl_cert.pem create mode 100644 tests/stdlib/thttpclient_ssl_key.pem create mode 100644 tests/untestable/thttpclient_ssl.nim create mode 100644 tests/untestable/thttpclient_ssl_disabled.nim create mode 100644 tests/untestable/thttpclient_ssl_env_var.nim diff --git a/.github/workflows/ci_ssl.yml b/.github/workflows/ci_ssl.yml new file mode 100644 index 0000000000000..fe5faded6ff5f --- /dev/null +++ b/.github/workflows/ci_ssl.yml @@ -0,0 +1,91 @@ +name: Nim SSL CI +on: + pull_request: + # Run only on changes on related files + paths: + - 'lib/pure/httpclient.nim' + - 'lib/pure/net.nim' + - 'lib/pure/ssl_certs.nim' + - 'lib/wrappers/openssl.nim' + - 'tests/stdlib/thttpclient_ssl*' + - 'tests/untestable/thttpclient_ssl*' + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [ubuntu-18.04, macos-10.15, windows-2019] + cpu: [amd64] + name: '${{ matrix.os }} (${{ matrix.cpu }})' + runs-on: ${{ matrix.os }} + steps: + - name: 'Checkout' + uses: actions/checkout@v2 + + - name: 'Checkout csources' + uses: actions/checkout@v2 + with: + repository: nim-lang/csources + path: csources + + - name: 'Install dependencies (Linux amd64)' + if: runner.os == 'Linux' && matrix.cpu == 'amd64' + run: | + sudo apt-fast update -qq + DEBIAN_FRONTEND='noninteractive' \ + sudo apt-fast install --no-install-recommends -y libssl1.1 + - name: 'Install dependencies (macOS)' + if: runner.os == 'macOS' + run: brew install make + - name: 'Install dependencies (Windows)' + if: runner.os == 'Windows' + shell: bash + run: | + mkdir dist + curl -L https://nim-lang.org/download/mingw64.7z -o dist/mingw64.7z + curl -L https://nim-lang.org/download/dlls.zip -o dist/dlls.zip + 7z x dist/mingw64.7z -odist + 7z x dist/dlls.zip -obin + echo "::add-path::${{ github.workspace }}/dist/mingw64/bin" + + - name: 'Add build binaries to PATH' + shell: bash + run: echo "::add-path::${{ github.workspace }}/bin" + + - name: 'Build 1-stage compiler from csources' + shell: bash + run: | + ncpu= + case '${{ runner.os }}' in + 'Linux') + ncpu=$(nproc) + ;; + 'macOS') + ncpu=$(sysctl -n hw.ncpu) + ;; + 'Windows') + ncpu=$NUMBER_OF_PROCESSORS + ;; + esac + [[ -z "$ncpu" || $ncpu -le 0 ]] && ncpu=1 + + make -C csources -j $ncpu CC=gcc ucpu='${{ matrix.cpu }}' + + - name: 'Build koch' + shell: bash + run: nim c koch + + - name: 'Build the real compiler' + shell: bash + run: ./koch boot + + - name: 'Run SSL nimDisableCertificateValidation integration tests' + shell: bash + run: nim c -d:nimDisableCertificateValidation -d:ssl -r -p:. tests/untestable/thttpclient_ssl_disabled.nim + + - name: 'Run SSL certificate check integration tests' + # Not supported on Windows due to old openssl version + if: runner.os != 'Windows' + shell: bash + run: nim c -d:ssl -p:. --threads:on -r tests/untestable/thttpclient_ssl.nim diff --git a/appveyor.yml b/appveyor.yml index e9407d1241778..5468ac88af36a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -29,6 +29,8 @@ install: - cd .. build_script: + - openssl version + - openssl version -d - bin\nim c koch - koch runCI diff --git a/changelog.md b/changelog.md index d3c85d6661b01..d5d1be95605a4 100644 --- a/changelog.md +++ b/changelog.md @@ -144,6 +144,8 @@ echo f empty. This was required for intuitive behaviour of the strscans module (see bug #13605). - `std/oswalkdir` was buggy, it's now deprecated and reuses `std/os` procs +- `net.newContext` now performs SSL Certificate checking on Linux and OSX. + Define `nimDisableCertificateValidation` to disable it globally. ## Language additions diff --git a/lib/pure/httpclient.nim b/lib/pure/httpclient.nim index b7bdc8d17f11e..e3e5a5c11ad19 100644 --- a/lib/pure/httpclient.nim +++ b/lib/pure/httpclient.nim @@ -119,6 +119,14 @@ ## You will also have to compile with ``ssl`` defined like so: ## ``nim c -d:ssl ...``. ## +## Certificate validation is NOT performed by default. +## This will change in future. +## +## A set of directories and files from the `ssl_certs `_ +## module are scanned to locate CA certificates. +## +## See `newContext `_ to tweak or disable certificate validation. +## ## Timeouts ## ======== ## @@ -552,6 +560,7 @@ proc newHttpClient*(userAgent = defUserAgent, maxRedirects = 5, ## default is 5. ## ## ``sslContext`` specifies the SSL context to use for HTTPS requests. + ## See `SSL/TLS support <##ssl-tls-support>`_ ## ## ``proxy`` specifies an HTTP proxy to use for this HTTP client's ## connections. diff --git a/lib/pure/net.nim b/lib/pure/net.nim index 0d4440242b86e..ca3d372599693 100644 --- a/lib/pure/net.nim +++ b/lib/pure/net.nim @@ -67,6 +67,8 @@ {.deadCodeElim: on.} # dce option deprecated import nativesockets, os, strutils, parseutils, times, sets, options, std/monotimes +from ospaths import getEnv +from ssl_certs import scanSSLCertificates export nativesockets.Port, nativesockets.`$`, nativesockets.`==` export Domain, SockType, Protocol @@ -83,7 +85,7 @@ when defineSsl: SslError* = object of Exception SslCVerifyMode* = enum - CVerifyNone, CVerifyPeer + CVerifyNone, CVerifyPeer, CVerifyPeerUseEnvVars SslProtVersion* = enum protSSLv2, protSSLv3, protTLSv1, protSSLv23 @@ -517,17 +519,30 @@ when defineSsl: raiseSSLError("Verification of private key file failed.") proc newContext*(protVersion = protSSLv23, verifyMode = CVerifyPeer, - certFile = "", keyFile = "", cipherList = "ALL"): SslContext = + certFile = "", keyFile = "", cipherList = "ALL", + caDir = "", caFile = ""): SSLContext = ## Creates an SSL context. ## ## Protocol version specifies the protocol to use. SSLv2, SSLv3, TLSv1 ## are available with the addition of ``protSSLv23`` which allows for ## compatibility with all of them. ## - ## There are currently only two options for verify mode; - ## one is ``CVerifyNone`` and with it certificates will not be verified - ## the other is ``CVerifyPeer`` and certificates will be verified for - ## it, ``CVerifyPeer`` is the safest choice. + ## There are three options for verify mode: + ## ``CVerifyNone``: certificates are not verified; + ## ``CVerifyPeer``: certificates are verified; + ## ``CVerifyPeerUseEnvVars``: certificates are verified and the optional + ## environment variables SSL_CERT_FILE and SSL_CERT_DIR are also used to + ## locate certificates + ## + ## The `nimDisableCertificateValidation` define overrides verifyMode and + ## disables certificate verification globally! + ## + ## CA certificates will be loaded, in the following order, from: + ## + ## - caFile, caDir, parameters, if set + ## - if `verifyMode` is set to ``CVerifyPeerUseEnvVars``, + ## the SSL_CERT_FILE and SSL_CERT_DIR environment variables are used + ## - a set of files and directories from the `ssl_certs `_ file. ## ## The last two parameters specify the certificate file path and the key file ## path, a server socket will most likely not work without these. @@ -550,18 +565,41 @@ when defineSsl: if newCTX.SSL_CTX_set_cipher_list(cipherList) != 1: raiseSSLError() - case verifyMode - of CVerifyPeer: - newCTX.SSL_CTX_set_verify(SSL_VERIFY_PEER, nil) - of CVerifyNone: + + when defined(nimDisableCertificateValidation) or defined(windows): newCTX.SSL_CTX_set_verify(SSL_VERIFY_NONE, nil) + else: + case verifyMode + of CVerifyPeer, CVerifyPeerUseEnvVars: + newCTX.SSL_CTX_set_verify(SSL_VERIFY_PEER, nil) + of CVerifyNone: + newCTX.SSL_CTX_set_verify(SSL_VERIFY_NONE, nil) + if newCTX == nil: raiseSSLError() discard newCTX.SSLCTXSetMode(SSL_MODE_AUTO_RETRY) newCTX.loadCertificates(certFile, keyFile) - result = SslContext(context: newCTX, referencedData: initSet[int](), + when not defined(nimDisableCertificateValidation) and not defined(windows): + if verifyMode != CVerifyNone: + # Use the caDir and caFile parameters if set + if caDir != "" or caFile != "": + if newCTX.SSL_CTX_load_verify_locations(caDir, caFile) != 0: + raise newException(IOError, "Failed to load SSL/TLS CA certificate(s).") + + else: + # Scan for certs in known locations. For CVerifyPeerUseEnvVars also scan + # the SSL_CERT_FILE and SSL_CERT_DIR env vars + var found = false + for fn in scanSSLCertificates(): + if newCTX.SSL_CTX_load_verify_locations(fn, "") == 0: + found = true + break + if not found: + raise newException(IOError, "No SSL/TLS CA certificates found.") + + result = SSLContext(context: newCTX, referencedData: initSet[int](), extraInternal: new(SslContextExtraInternal)) proc getExtraInternal(ctx: SslContext): SslContextExtraInternal = @@ -645,6 +683,7 @@ when defineSsl: ## This must be called on an unconnected socket; an SSL session will ## be started when the socket is connected. ## + ## FIXME: ## **Disclaimer**: This code is not well tested, may be very unsafe and ## prone to security vulnerabilities. @@ -660,7 +699,25 @@ when defineSsl: if SSL_set_fd(socket.sslHandle, socket.fd) != 1: raiseSSLError() - proc wrapConnectedSocket*(ctx: SslContext, socket: Socket, + proc checkCertName(socket: Socket, hostname: string) = + ## Check if the certificate Subject Alternative Name (SAN) or Subject CommonName (CN) matches hostname. + ## Wildcards match only in the left-most label. + ## When name starts with a dot it will be matched by a certificate valid for any subdomain + when not defined(nimDisableCertificateValidation) and not defined(windows): + assert socket.isSSL + let certificate = socket.sslHandle.SSL_get_peer_certificate() + if certificate.isNil: + raiseSSLError("No SSL certificate found.") + + const X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT = 0x1.cuint + const size = 1024 + var peername: string = newString(size) + let match = certificate.X509_check_host(hostname.cstring, hostname.len.cint, + X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT, peername) + if match != 1: + raiseSSLError("SSL Certificate check failed.") + + proc wrapConnectedSocket*(ctx: SSLContext, socket: Socket, handshake: SslHandshakeType, hostname: string = "") = ## Wraps a connected socket in an SSL context. This function effectively @@ -671,6 +728,7 @@ when defineSsl: ## This should be called on a connected socket, and will perform ## an SSL handshake immediately. ## + ## FIXME: ## **Disclaimer**: This code is not well tested, may be very unsafe and ## prone to security vulnerabilities. wrapSocket(ctx, socket) @@ -682,6 +740,9 @@ when defineSsl: discard SSL_set_tlsext_host_name(socket.sslHandle, hostname) let ret = SSL_connect(socket.sslHandle) socketError(socket, ret) + when not defined(nimDisableCertificateValidation) and not defined(windows): + if hostname.len > 0 and not isIpAddress(hostname): + socket.checkCertName(hostname) of handshakeAsServer: let ret = SSL_accept(socket.sslHandle) socketError(socket, ret) @@ -1638,6 +1699,9 @@ proc connect*(socket: Socket, address: string, let ret = SSL_connect(socket.sslHandle) socketError(socket, ret) + when not defined(nimDisableCertificateValidation) and not defined(windows): + if not isIpAddress(address): + socket.checkCertName(address) proc connectAsync(socket: Socket, name: string, port = Port(0), af: Domain = AF_INET) {.tags: [ReadIOEffect].} = diff --git a/lib/pure/ssl_certs.nim b/lib/pure/ssl_certs.nim new file mode 100644 index 0000000000000..7b1550004590f --- /dev/null +++ b/lib/pure/ssl_certs.nim @@ -0,0 +1,97 @@ +# +# +# Nim's Runtime Library +# (c) Copyright 2017 Nim contributors +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# +## Scan for SSL/TLS CA certificates on disk +## The default locations can be overridden using the SSL_CERT_FILE and +## SSL_CERT_DIR environment variables. + +import os, strutils +from ospaths import existsEnv, getEnv +import strutils + +# SECURITY: this unnecessarily scans through dirs/files regardless of the +# actual host OS/distribution. Hopefully all the paths are writeble only by +# root. + +# FWIW look for files before scanning entire dirs. + +const certificate_paths = [ + # Debian, Ubuntu, Arch: maintained by update-ca-certificates, SUSE, Gentoo + # NetBSD (security/mozilla-rootcerts) + # SLES10/SLES11, https://golang.org/issue/12139 + "/etc/ssl/certs/ca-certificates.crt", + # OpenSUSE + "/etc/ssl/ca-bundle.pem", + # Red Hat 5+, Fedora, Centos + "/etc/pki/tls/certs/ca-bundle.crt", + # Red Hat 4 + "/usr/share/ssl/certs/ca-bundle.crt", + # FreeBSD (security/ca-root-nss package) + "/usr/local/share/certs/ca-root-nss.crt", + # CentOS/RHEL 7 + "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", + # OpenBSD, FreeBSD (optional symlink) + "/etc/ssl/cert.pem", + # Mac OS X + "/System/Library/OpenSSL/certs/cert.pem", + # Fedora/RHEL + "/etc/pki/tls/certs", + # Android + "/system/etc/security/cacerts", + # FreeBSD + "/usr/local/share/certs", + # NetBSD + "/etc/openssl/certs", +] + +iterator scanSSLCertificates*(useEnvVars = false): string = + ## Scan for SSL/TLS CA certificates on disk. + ## + ## if `useEnvVars` is true, the SSL_CERT_FILE and SSL_CERT_DIR + ## environment variables can be used to override the certificate + ## directories to scan or specify a CA certificate file. + if existsEnv("SSL_CERT_FILE"): + yield getEnv("SSL_CERT_FILE") + + elif existsEnv("SSL_CERT_DIR"): + let p = getEnv("SSL_CERT_DIR") + for fn in joinPath(p, "*").walkFiles(): + yield fn + + else: + for p in certificate_paths: + if p.endsWith(".pem") or p.endsWith(".crt"): + if existsFile(p): + yield p + elif existsDir(p): + for fn in joinPath(p, "*").walkFiles(): + yield fn + +# Certificates management on windows +# when defined(windows) or defined(nimdoc): +# +# import openssl +# +# type +# PCCertContext {.final, pure.} = pointer +# X509 {.final, pure.} = pointer +# CertStore {.final, pure.} = pointer +# +# # OpenSSL cert store +# +# {.push stdcall, dynlib: "kernel32", importc.} +# +# proc CertOpenSystemStore*(hprov: pointer=nil, szSubsystemProtocol: cstring): CertStore +# +# proc CertEnumCertificatesInStore*(hCertStore: CertStore, pPrevCertContext: PCCertContext): pointer +# +# proc CertFreeCertificateContext*(pContext: PCCertContext): bool +# +# proc CertCloseStore*(hCertStore:CertStore, flags:cint): bool +# +# {.pop.} diff --git a/lib/wrappers/openssl.nim b/lib/wrappers/openssl.nim index 4a53fba808f9c..789b3611c8385 100644 --- a/lib/wrappers/openssl.nim +++ b/lib/wrappers/openssl.nim @@ -647,3 +647,74 @@ proc md5_Str*(str: string): string = when defined(nimHasStyleChecks): {.pop.} + + +# Certificate validation +# On old openSSL version some of these symbols are not available +when not defined(nimDisableCertificateValidation) and not defined(windows): + + proc SSL_get_peer_certificate*(ssl: SslCtx): PX509{.cdecl, dynlib: DLLSSLName, + importc.} + + proc X509_get_subject_name*(a: PX509): PX509_NAME{.cdecl, dynlib: DLLSSLName, importc.} + + proc X509_get_issuer_name*(a: PX509): PX509_NAME{.cdecl, dynlib: DLLUtilName, importc.} + + proc X509_NAME_oneline*(a: PX509_NAME, buf: cstring, size: cint): cstring {. + cdecl, dynlib:DLLSSLName, importc.} + + proc X509_NAME_get_text_by_NID*(subject:cstring, NID: cint, buf: cstring, size: cint): cint{. + cdecl, dynlib:DLLSSLName, importc.} + + proc X509_check_host*(cert: PX509, name: cstring, namelen: cint, flags:cuint, peername: cstring): cint {.cdecl, dynlib: DLLSSLName, importc.} + + # Certificates store + + type PX509_STORE* = SslPtr + type PX509_OBJECT* = SslPtr + + {.push callconv:cdecl, dynlib:DLLUtilName, importc.} + + proc X509_OBJECT_new*(): PX509_OBJECT + proc X509_OBJECT_free*(a: PX509_OBJECT) + + proc X509_STORE_new*(): PX509_STORE + proc X509_STORE_free*(v: PX509_STORE) + proc X509_STORE_lock*(ctx: PX509_STORE): cint + proc X509_STORE_unlock*(ctx: PX509_STORE): cint + proc X509_STORE_up_ref*(v: PX509_STORE): cint + proc X509_STORE_set_flags*(ctx: PX509_STORE; flags: culong): cint + proc X509_STORE_set_purpose*(ctx: PX509_STORE; purpose: cint): cint + proc X509_STORE_set_trust*(ctx: PX509_STORE; trust: cint): cint + proc X509_STORE_add_cert*(ctx: PX509_STORE; x: PX509): cint + + proc d2i_X509*(px: ptr PX509, i: ptr ptr cuchar, len: cint): PX509 + + proc i2d_X509*(cert: PX509; o: ptr ptr cuchar): cint + + {.pop.} + + proc d2i_X509*(b: string): PX509 = + ## decode DER/BER bytestring into X.509 certificate struct + var bb = b.cstring + let i = cast[ptr ptr cuchar](addr bb) + let ret = d2i_X509(addr result, i, b.len.cint) + if ret.isNil: + raise newException(Exception, "X.509 certificate decoding failed") + + proc i2d_X509*(cert: PX509): string = + ## encode `cert` to DER string + let encoded_length = i2d_X509(cert, nil) + result = newString(encoded_length) + var q = result.cstring + let o = cast[ptr ptr cuchar](addr q) + let length = i2d_X509(cert, o) + if length.int <= 0: + raise newException(Exception, "X.509 certificate encoding failed") + + when isMainModule: + # A simple certificate test + let certbytes = readFile("certificate.der") + let cert = d2i_X509(certbytes) + let encoded = cert.i2d_X509() + assert encoded == certbytes diff --git a/tests/stdlib/thttpclient_ssl.nim b/tests/stdlib/thttpclient_ssl.nim new file mode 100644 index 0000000000000..f247ae4426edd --- /dev/null +++ b/tests/stdlib/thttpclient_ssl.nim @@ -0,0 +1,126 @@ +discard """ + cmd: "nim $target --threads:on -d:ssl $options $file" +""" + +# Nim - Basic SSL integration tests +# (c) Copyright 2018 Nim contributors +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# +## Warning: this test performs local networking. +## Test with: +## ./bin/nim c -d:ssl -p:. --threads:on -r tests/stdlib/thttpclient_ssl.nim + +when not defined(windows): + # Disabled on Windows due to old OpenSSL version + + import + httpclient, + net, + openssl, + os, + strutils, + threadpool, + times, + unittest + + # bogus self-signed certificate + const + certFile = "tests/stdlib/thttpclient_ssl_cert.pem" + keyFile = "tests/stdlib/thttpclient_ssl_key.pem" + + proc log(msg: string) = + when defined(ssldebug): + echo " [" & $epochTime() & "] " & msg + # FIXME + echo " [" & $epochTime() & "] " & msg + discard + + proc runServer(port: Port): bool {.thread.} = + ## Run a trivial HTTPS server in a {.thread.} + ## Exit after serving one request + + var socket = newSocket() + socket.setSockOpt(OptReusePort, true) + socket.bindAddr(port) + + var ctx = newContext(certFile=certFile, keyFile=keyFile) + + ## Handle one connection + socket.listen() + + var client: Socket + var address = "" + + log "server: ready" + socket.acceptAddr(client, address) + log "server: incoming connection" + + var ssl: SslPtr = SSL_new(ctx.context) + discard SSL_set_fd(ssl, client.getFd()) + log "server: accepting connection" + if SSL_accept(ssl) <= 0: + ERR_print_errors_fp(stderr) + else: + const reply = "HTTP/1.0 200 OK\r\nServer: test\r\nContent-type: text/html\r\nContent-Length: 0\r\n\r\n" + log "server: sending reply" + discard SSL_write(ssl, reply.cstring, reply.len) + + log "server: receiving a line" + let line = client.recvLine() + log "server: received $# bytes" % $line.len + log "closing" + SSL_free(ssl) + close(client) + close(socket) + log "server: exited" + + + suite "SSL self signed certificate check": + + test "TCP socket": + const port = 12347.Port + let t = spawn runServer(port) + sleep(100) + var sock = newSocket() + var ctx = newContext() + ctx.wrapSocket(sock) + try: + log "client: connect" + sock.connect("127.0.0.1", port) + fail() + except: + let msg = getCurrentExceptionMsg() + check(msg.contains("certificate verify failed")) + + test "HttpClient default: no check": + const port = 12345.Port + let t = spawn runServer(port) + sleep(100) + + var client = newHttpClient() + try: + log "client: connect" + discard client.getContent("https://127.0.0.1:12345") + except: + let msg = getCurrentExceptionMsg() + log "client: unexpected exception: " & msg + fail() + + test "HttpClient with CVerifyPeer": + const port = 12346.Port + let t = spawn runServer(port) + sleep(100) + + var client = newHttpClient(sslContext=newContext(verifyMode=CVerifyPeer)) + try: + log "client: connect" + discard client.getContent("https://127.0.0.1:12346") + log "getContent should have raised an exception" + fail() + except: + let msg = getCurrentExceptionMsg() + log "client: exception: " & msg + # SSL_shutdown:shutdown while in init + check(msg.contains("shutdown while in init") or msg.contains("alert number 48")) diff --git a/tests/stdlib/thttpclient_ssl_cert.pem b/tests/stdlib/thttpclient_ssl_cert.pem new file mode 100644 index 0000000000000..f15c15c525f32 --- /dev/null +++ b/tests/stdlib/thttpclient_ssl_cert.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCTCCAvGgAwIBAgIURYQOmGzeh3Vy7Gk6Go4uAPwcNwAwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTE5MDEyMzAwMTgzNFoXDTQ2MDYw +OTAwMTgzNFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAzoEVEl7yqY+RqIagXDD4JB7LyONDvh8aJvBMnJVBgjaL +JdkfQjvPGUzUkEbU5nc6u7lqFxzEv7hXrssQCB7TwJwfS2PT1Rj14IFlYPyw4DEe +P1RVS/awurtv3jwumarVl7LR+IQfo59kJ/P8jZt8H3HscDbyhXcHeOWI6q+XlfdV +mTUJVvABdUuOiIFjgfFVTpo+CKxy7c5caRDK7g1s9xB1/M9PUfJvHY1WrBWFOZf0 +Bl8iwn+ahuxfIVqsFL9leqLykgi1f4L20p7RaAK95TXCo3CszZm4Fsw9zhzkjoU7 +2h0nuYl197LZvRs3u/JJjzZERmsfVPIs5BtO8/MN1MvRn6hIGU5Q3kOVWqWxSkSl +njrf+uwUdn/24uSCnygNeDuJzwW/2q4N9YI3oovqNIGpkT3FbAm7UKwI4lwhwmqw +7WH+92ELj0BinmsMMRPD2OqvK+vzLVqwUIQkYug+Hjys6QGXMlrL0krrj7XOKSc3 +SvZa4j0S/Y5CKkw5xuZXxITsdaV6hGi3d/kuT+1ttOSfIIXJXDEiu4pYRfziKU1a +8EhHMEajEi6ueLw7QmEPVx398erRwiUuP2y43yZ4mwVwvN3i5jlVztl4XsglDmQ+ +hahstVdMMA34K2rK0U8q8YjdYm+z99NmGEPYrS6Qnpr1xrICN83FOWFI0k7ttyMC +AwEAAaNTMFEwHQYDVR0OBBYEFLqMY6eP3h3gu+ANs77xDBRnElxyMB8GA1UdIwQY +MBaAFLqMY6eP3h3gu+ANs77xDBRnElxyMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggIBAJS+wyy0r+tVAlCa6V/xxlCDtW9n6L2nsqJXEjME0VvwGs3m +ima68LyTQJqCSjjxSotaNOYKzUu4vRA3JssV+fUDR+NpmhpRuM74XmO05HUQkp9U +dBEHyXp2aRQ9LSdvHo5D+RW+J4sHFb3PbU8NPx/t5Dg7il92S2QJQz1jNl+Nezc6 +2O8Vt1YbvWXfqM47URTpnQbWoo38pI44AgAuW3QagucKWsyounmhx65XcdtLn99g +oZt496pU+hBpYu/IpXuBKNC4FvOrXTWAPkAbbYP39UFyiKwIyTosK+qdbhBlt1xi +bBPn6N1W9L2BvUwM8fEB/qBuR9UfcMsIYJsWbbXMfyeF6lbaP7xD01rm+yU5PMMI +Co40abixMntz4J3T2ixdCptf0He1U/UegOHwG1ZGgQzvOG6qI/xkNktDaSA75KR7 +BvPV1CmZC4ovVo1L4STrwnoRz5J49PNOHi9Okj9zJ99H7nsmsK16oxpIYkYHJWn+ +45jpG8SlDp7oev1OGGk/z+ZOTz+LcNxyvsRQVN8w5zNmjCSWiGqz+UUgppCZg8qd +ECWokNQ5Lr20t1whynrX5bH0l887WPCQmm5VduRoyKFGhCRBSzcCtowSpiwZglUk +CV0jgFKoteItdzZgsND5I1GaNOxZlnK3wN4H0pgZv7HlW6SP1OYd2Y67waJ7 +-----END CERTIFICATE----- diff --git a/tests/stdlib/thttpclient_ssl_key.pem b/tests/stdlib/thttpclient_ssl_key.pem new file mode 100644 index 0000000000000..6ab04122c62b9 --- /dev/null +++ b/tests/stdlib/thttpclient_ssl_key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +6THbQsJpR5AalTVvXs3X1RnCLnHiNYg= +-----END PRIVATE KEY----- diff --git a/tests/untestable/thttpclient_ssl.nim b/tests/untestable/thttpclient_ssl.nim new file mode 100644 index 0000000000000..3744df92dac0a --- /dev/null +++ b/tests/untestable/thttpclient_ssl.nim @@ -0,0 +1,203 @@ +# +# +# Nim - SSL integration tests +# (c) Copyright 2017 Nim contributors +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# +## Warning: this test performs external networking. +## Test with: +## ./bin/nim c -d:ssl -p:. --threads:on -r tests/untestable/thttpclient_ssl.nim +## +## See https://github.com/FedericoCeratto/ssl-comparison/blob/master/README.md +## for a comparison with other clients. + +import + httpclient, + net, + strutils, + threadpool, + unittest + + +type + # bad and dubious tests should not pass SSL validation + # "_broken" mark the test as skipped. Some tests have different + # behavior depending on OS and SSL version! + # TODO: chase and fix the broken tests + Category = enum + good, bad, dubious, good_broken, bad_broken, dubious_broken + CertTest = tuple[url:string, category:Category, desc: string] + +const certificate_tests: array[0..55, CertTest] = [ + ("https://wrong.host.badssl.com/", bad, "wrong.host"), + ("https://captive-portal.badssl.com/", bad, "captive-portal"), + ("https://expired.badssl.com/", bad, "expired"), + ("https://google.com/", good, "good"), + ("https://self-signed.badssl.com/", bad, "self-signed"), + ("https://untrusted-root.badssl.com/", bad, "untrusted-root"), + ("https://revoked.badssl.com/", bad_broken, "revoked"), + ("https://pinning-test.badssl.com/", bad_broken, "pinning-test"), + ("https://no-common-name.badssl.com/", dubious_broken, "no-common-name"), + ("https://no-subject.badssl.com/", dubious_broken, "no-subject"), + ("https://incomplete-chain.badssl.com/", dubious, "incomplete-chain"), + ("https://sha1-intermediate.badssl.com/", bad_broken, "sha1-intermediate"), + ("https://sha256.badssl.com/", good, "sha256"), + ("https://sha384.badssl.com/", good, "sha384"), + ("https://sha512.badssl.com/", good, "sha512"), + ("https://1000-sans.badssl.com/", good, "1000-sans"), + ("https://10000-sans.badssl.com/", good_broken, "10000-sans"), + ("https://ecc256.badssl.com/", good, "ecc256"), + ("https://ecc384.badssl.com/", good, "ecc384"), + ("https://rsa2048.badssl.com/", good, "rsa2048"), + ("https://rsa8192.badssl.com/", dubious_broken, "rsa8192"), + ("http://http.badssl.com/", good, "regular http"), + ("https://http.badssl.com/", bad_broken, "http on https URL"), # FIXME + ("https://cbc.badssl.com/", dubious_broken, "cbc"), + ("https://rc4-md5.badssl.com/", bad, "rc4-md5"), + ("https://rc4.badssl.com/", bad, "rc4"), + ("https://3des.badssl.com/", bad, "3des"), + ("https://null.badssl.com/", bad, "null"), + ("https://mozilla-old.badssl.com/", bad_broken, "mozilla-old"), + ("https://mozilla-intermediate.badssl.com/", dubious_broken, "mozilla-intermediate"), + ("https://mozilla-modern.badssl.com/", good, "mozilla-modern"), + ("https://dh480.badssl.com/", bad, "dh480"), + ("https://dh512.badssl.com/", bad, "dh512"), + ("https://dh1024.badssl.com/", dubious_broken, "dh1024"), + ("https://dh2048.badssl.com/", good, "dh2048"), + ("https://dh-small-subgroup.badssl.com/", bad_broken, "dh-small-subgroup"), + ("https://dh-composite.badssl.com/", bad_broken, "dh-composite"), + ("https://static-rsa.badssl.com/", dubious_broken, "static-rsa"), + ("https://tls-v1-0.badssl.com:1010/", dubious_broken, "tls-v1-0"), + ("https://tls-v1-1.badssl.com:1011/", dubious_broken, "tls-v1-1"), + ("https://invalid-expected-sct.badssl.com/", bad_broken, "invalid-expected-sct"), + ("https://hsts.badssl.com/", good, "hsts"), + ("https://upgrade.badssl.com/", good, "upgrade"), + ("https://preloaded-hsts.badssl.com/", good, "preloaded-hsts"), + ("https://subdomain.preloaded-hsts.badssl.com/", bad, "subdomain.preloaded-hsts"), + ("https://https-everywhere.badssl.com/", good, "https-everywhere"), + ("https://long-extended-subdomain-name-containing-many-letters-and-dashes.badssl.com/", good, + "long-extended-subdomain-name-containing-many-letters-and-dashes"), + ("https://longextendedsubdomainnamewithoutdashesinordertotestwordwrapping.badssl.com/", good, + "longextendedsubdomainnamewithoutdashesinordertotestwordwrapping"), + ("https://superfish.badssl.com/", bad, "(Lenovo) Superfish"), + ("https://edellroot.badssl.com/", bad, "(Dell) eDellRoot"), + ("https://dsdtestprovider.badssl.com/", bad, "(Dell) DSD Test Provider"), + ("https://preact-cli.badssl.com/", bad, "preact-cli"), + ("https://webpack-dev-server.badssl.com/", bad, "webpack-dev-server"), + ("https://mitm-software.badssl.com/", bad, "mitm-software"), + ("https://sha1-2016.badssl.com/", dubious, "sha1-2016"), + ("https://sha1-2017.badssl.com/", bad, "sha1-2017"), +] + + +template evaluate(exception_msg: string, category: Category, desc: string) = + # Evaluate test outcome. Testes flagged as _broken are evaluated and skipped + let raised = (exception_msg.len > 0) + let should_not_raise = category in {good, dubious_broken, bad_broken} + if should_not_raise xor raised: + # we are seeing a known behavior + if category in {good_broken, dubious_broken, bad_broken}: + skip() + if raised: + check exception_msg == "No SSL certificate found." or + exception_msg == "SSL Certificate check failed." or + exception_msg.contains("certificate verify failed") or + exception_msg.contains("key too small") or + exception_msg.contains "shutdown while in init" + + else: + # this is unexpected + if raised: + echo " $# ($#) raised: $#" % [desc, $category, exception_msg] + else: + echo " $# ($#) did not raise" % [desc, $category] + if category in {good, dubious, bad}: + fail() + + +suite "SSL certificate check - httpclient": + + for i, ct in certificate_tests: + + test ct.desc: + var ctx = newContext(verifyMode=CVerifyPeer) + var client = newHttpClient(sslContext=ctx) + let exception_msg = + try: + let a = $client.getContent(ct.url) + "" + except: + getCurrentExceptionMsg() + + evaluate(exception_msg, ct.category, ct.desc) + + + +# threaded tests + + +type + TTOutcome = ref object + desc, exception_msg: string + category: Category + +proc run_t_test(ct: CertTest): TTOutcome {.thread.} = + ## Run test in a {.thread.} - return by ref + result = TTOutcome(desc:ct.desc, exception_msg:"", category: ct.category) + try: + var ctx = newContext(verifyMode=CVerifyPeer) + var client = newHttpClient(sslContext=ctx) + let a = $client.getContent(ct.url) + except: + result.exception_msg = getCurrentExceptionMsg() + + +suite "SSL certificate check - httpclient - threaded": + + # Spawn threads before the "test" blocks + var outcomes = newSeq[FlowVar[TTOutcome]](certificate_tests.len) + for i, ct in certificate_tests: + let t = spawn run_t_test(ct) + outcomes[i] = t + + # create "test" blocks and handle thread outputs + for t in outcomes: + let outcome = ^t # wait for a thread to terminate + + test outcome.desc: + + evaluate(outcome.exception_msg, outcome.category, outcome.desc) + + +# net tests + + +type NetSocketTest = tuple[hostname: string, port: Port, category:Category, desc: string] +const net_tests:array[0..3, NetSocketTest] = [ + ("imap.gmail.com", 993.Port, good, "IMAP"), + ("wrong.host.badssl.com", 443.Port, bad, "wrong.host"), + ("captive-portal.badssl.com", 443.Port, bad, "captive-portal"), + ("expired.badssl.com", 443.Port, bad, "expired"), +] +# TODO: ("null.badssl.com", 443.Port, bad_broken, "null"), + + +suite "SSL certificate check - sockets": + + for ct in net_tests: + + test ct.desc: + + var sock = newSocket() + var ctx = newContext() + ctx.wrapSocket(sock) + let exception_msg = + try: + sock.connect(ct.hostname, ct.port) + "" + except: + getCurrentExceptionMsg() + + evaluate(exception_msg, ct.category, ct.desc) diff --git a/tests/untestable/thttpclient_ssl_disabled.nim b/tests/untestable/thttpclient_ssl_disabled.nim new file mode 100644 index 0000000000000..4d4ede4de35eb --- /dev/null +++ b/tests/untestable/thttpclient_ssl_disabled.nim @@ -0,0 +1,40 @@ +# +# Nim - SSL integration tests +# (c) Copyright 2017 Nim contributors +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# +## Warning: this test performs external networking. +## Compile and run with: +## ./bin/nim c -d:nimDisableCertificateValidation -d:ssl -r -p:. tests/untestable/thttpclient_ssl_disabled.nim + +import httpclient, + net, + unittest, + ospaths + +from strutils import contains + +const expired = "https://expired.badssl.com/" + +doAssert defined(nimDisableCertificateValidation) + +suite "SSL certificate check - disabled": + + test "httpclient in insecure mode": + var ctx = newContext(verifyMode = CVerifyPeer) + var client = newHttpClient(sslContext = ctx) + let a = $client.getContent(expired) + + test "httpclient in insecure mode": + var ctx = newContext(verifyMode = CVerifyPeerUseEnvVars) + var client = newHttpClient(sslContext = ctx) + let a = $client.getContent(expired) + + test "net socket in insecure mode": + var sock = newSocket() + var ctx = newContext(verifyMode = CVerifyPeerUseEnvVars) + ctx.wrapSocket(sock) + sock.connect("expired.badssl.com", 443.Port) + sock.close diff --git a/tests/untestable/thttpclient_ssl_env_var.nim b/tests/untestable/thttpclient_ssl_env_var.nim new file mode 100644 index 0000000000000..32af435799e83 --- /dev/null +++ b/tests/untestable/thttpclient_ssl_env_var.nim @@ -0,0 +1,74 @@ +# +# Nim - SSL integration tests +# (c) Copyright 2017 Nim contributors +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# +## Warning: this test performs external networking. +## Compile with: +## ./bin/nim c -d:ssl -p:. tests/untestable/thttpclient_ssl_env_var.nim +## +## Test with: +## SSL_CERT_FILE=BogusInexistentFileName tests/untestable/thttpclient_ssl_env_var +## SSL_CERT_DIR=BogusInexistentDirName tests/untestable/thttpclient_ssl_env_var + +import httpclient, unittest, ospaths +from net import newSocket, newContext, wrapSocket, connect, close, Port, + CVerifyPeerUseEnvVars +from strutils import contains + +const + expired = "https://expired.badssl.com/" + good = "https://google.com/" + + +suite "SSL certificate check": + + test "httpclient with inexistent file": + if existsEnv("SSL_CERT_FILE"): + var ctx = newContext(verifyMode=CVerifyPeerUseEnvVars) + var client = newHttpClient(sslContext=ctx) + checkpoint("Client created") + check client.getContent("https://google.com").contains("doctype") + checkpoint("Google ok") + try: + let a = $client.getContent(good) + echo "Connection should have failed" + fail() + except: + echo getCurrentExceptionMsg() + check getCurrentExceptionMsg().contains("certificate verify failed") + + elif existsEnv("SSL_CERT_DIR"): + try: + var ctx = newContext(verifyMode=CVerifyPeerUseEnvVars) + var client = newHttpClient(sslContext=ctx) + echo "Should have raised 'No SSL/TLS CA certificates found.'" + fail() + except: + check getCurrentExceptionMsg() == + "No SSL/TLS CA certificates found." + + test "net socket with inexistent file": + if existsEnv("SSL_CERT_FILE"): + var sock = newSocket() + var ctx = newContext(verifyMode=CVerifyPeerUseEnvVars) + ctx.wrapSocket(sock) + checkpoint("Socket created") + try: + sock.connect("expired.badssl.com", 443.Port) + fail() + except: + sock.close + check getCurrentExceptionMsg().contains("certificate verify failed") + + elif existsEnv("SSL_CERT_DIR"): + var sock = newSocket() + checkpoint("Socket created") + try: + var ctx = newContext(verifyMode=CVerifyPeerUseEnvVars) # raises here + fail() + except: + check getCurrentExceptionMsg() == + "No SSL/TLS CA certificates found."