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
This commit is contained in:
parent
1d665adecd
commit
5b85444244
13 changed files with 872 additions and 12 deletions
91
.github/workflows/ci_ssl.yml
vendored
Normal file
91
.github/workflows/ci_ssl.yml
vendored
Normal file
|
|
@ -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
|
||||||
|
|
@ -29,6 +29,8 @@ install:
|
||||||
- cd ..
|
- cd ..
|
||||||
|
|
||||||
build_script:
|
build_script:
|
||||||
|
- openssl version
|
||||||
|
- openssl version -d
|
||||||
- bin\nim c koch
|
- bin\nim c koch
|
||||||
- koch runCI
|
- koch runCI
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,8 @@ echo f
|
||||||
empty. This was required for intuitive behaviour of the strscans module
|
empty. This was required for intuitive behaviour of the strscans module
|
||||||
(see bug #13605).
|
(see bug #13605).
|
||||||
- `std/oswalkdir` was buggy, it's now deprecated and reuses `std/os` procs
|
- `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
|
## Language additions
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,14 @@
|
||||||
## You will also have to compile with ``ssl`` defined like so:
|
## You will also have to compile with ``ssl`` defined like so:
|
||||||
## ``nim c -d:ssl ...``.
|
## ``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 <ssl_certs.html>`_
|
||||||
|
## module are scanned to locate CA certificates.
|
||||||
|
##
|
||||||
|
## See `newContext <net.html#newContext>`_ to tweak or disable certificate validation.
|
||||||
|
##
|
||||||
## Timeouts
|
## Timeouts
|
||||||
## ========
|
## ========
|
||||||
##
|
##
|
||||||
|
|
@ -552,6 +560,7 @@ proc newHttpClient*(userAgent = defUserAgent, maxRedirects = 5,
|
||||||
## default is 5.
|
## default is 5.
|
||||||
##
|
##
|
||||||
## ``sslContext`` specifies the SSL context to use for HTTPS requests.
|
## ``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
|
## ``proxy`` specifies an HTTP proxy to use for this HTTP client's
|
||||||
## connections.
|
## connections.
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,8 @@
|
||||||
{.deadCodeElim: on.} # dce option deprecated
|
{.deadCodeElim: on.} # dce option deprecated
|
||||||
import nativesockets, os, strutils, parseutils, times, sets, options,
|
import nativesockets, os, strutils, parseutils, times, sets, options,
|
||||||
std/monotimes
|
std/monotimes
|
||||||
|
from ospaths import getEnv
|
||||||
|
from ssl_certs import scanSSLCertificates
|
||||||
export nativesockets.Port, nativesockets.`$`, nativesockets.`==`
|
export nativesockets.Port, nativesockets.`$`, nativesockets.`==`
|
||||||
export Domain, SockType, Protocol
|
export Domain, SockType, Protocol
|
||||||
|
|
||||||
|
|
@ -83,7 +85,7 @@ when defineSsl:
|
||||||
SslError* = object of Exception
|
SslError* = object of Exception
|
||||||
|
|
||||||
SslCVerifyMode* = enum
|
SslCVerifyMode* = enum
|
||||||
CVerifyNone, CVerifyPeer
|
CVerifyNone, CVerifyPeer, CVerifyPeerUseEnvVars
|
||||||
|
|
||||||
SslProtVersion* = enum
|
SslProtVersion* = enum
|
||||||
protSSLv2, protSSLv3, protTLSv1, protSSLv23
|
protSSLv2, protSSLv3, protTLSv1, protSSLv23
|
||||||
|
|
@ -517,17 +519,30 @@ when defineSsl:
|
||||||
raiseSSLError("Verification of private key file failed.")
|
raiseSSLError("Verification of private key file failed.")
|
||||||
|
|
||||||
proc newContext*(protVersion = protSSLv23, verifyMode = CVerifyPeer,
|
proc newContext*(protVersion = protSSLv23, verifyMode = CVerifyPeer,
|
||||||
certFile = "", keyFile = "", cipherList = "ALL"): SslContext =
|
certFile = "", keyFile = "", cipherList = "ALL",
|
||||||
|
caDir = "", caFile = ""): SSLContext =
|
||||||
## Creates an SSL context.
|
## Creates an SSL context.
|
||||||
##
|
##
|
||||||
## Protocol version specifies the protocol to use. SSLv2, SSLv3, TLSv1
|
## Protocol version specifies the protocol to use. SSLv2, SSLv3, TLSv1
|
||||||
## are available with the addition of ``protSSLv23`` which allows for
|
## are available with the addition of ``protSSLv23`` which allows for
|
||||||
## compatibility with all of them.
|
## compatibility with all of them.
|
||||||
##
|
##
|
||||||
## There are currently only two options for verify mode;
|
## There are three options for verify mode:
|
||||||
## one is ``CVerifyNone`` and with it certificates will not be verified
|
## ``CVerifyNone``: certificates are not verified;
|
||||||
## the other is ``CVerifyPeer`` and certificates will be verified for
|
## ``CVerifyPeer``: certificates are verified;
|
||||||
## it, ``CVerifyPeer`` is the safest choice.
|
## ``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 <ssl_certs.html>`_ file.
|
||||||
##
|
##
|
||||||
## The last two parameters specify the certificate file path and the key 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.
|
## 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:
|
if newCTX.SSL_CTX_set_cipher_list(cipherList) != 1:
|
||||||
raiseSSLError()
|
raiseSSLError()
|
||||||
case verifyMode
|
|
||||||
of CVerifyPeer:
|
when defined(nimDisableCertificateValidation) or defined(windows):
|
||||||
newCTX.SSL_CTX_set_verify(SSL_VERIFY_PEER, nil)
|
|
||||||
of CVerifyNone:
|
|
||||||
newCTX.SSL_CTX_set_verify(SSL_VERIFY_NONE, nil)
|
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:
|
if newCTX == nil:
|
||||||
raiseSSLError()
|
raiseSSLError()
|
||||||
|
|
||||||
discard newCTX.SSLCTXSetMode(SSL_MODE_AUTO_RETRY)
|
discard newCTX.SSLCTXSetMode(SSL_MODE_AUTO_RETRY)
|
||||||
newCTX.loadCertificates(certFile, keyFile)
|
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))
|
extraInternal: new(SslContextExtraInternal))
|
||||||
|
|
||||||
proc getExtraInternal(ctx: SslContext): SslContextExtraInternal =
|
proc getExtraInternal(ctx: SslContext): SslContextExtraInternal =
|
||||||
|
|
@ -645,6 +683,7 @@ when defineSsl:
|
||||||
## This must be called on an unconnected socket; an SSL session will
|
## This must be called on an unconnected socket; an SSL session will
|
||||||
## be started when the socket is connected.
|
## be started when the socket is connected.
|
||||||
##
|
##
|
||||||
|
## FIXME:
|
||||||
## **Disclaimer**: This code is not well tested, may be very unsafe and
|
## **Disclaimer**: This code is not well tested, may be very unsafe and
|
||||||
## prone to security vulnerabilities.
|
## prone to security vulnerabilities.
|
||||||
|
|
||||||
|
|
@ -660,7 +699,25 @@ when defineSsl:
|
||||||
if SSL_set_fd(socket.sslHandle, socket.fd) != 1:
|
if SSL_set_fd(socket.sslHandle, socket.fd) != 1:
|
||||||
raiseSSLError()
|
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,
|
handshake: SslHandshakeType,
|
||||||
hostname: string = "") =
|
hostname: string = "") =
|
||||||
## Wraps a connected socket in an SSL context. This function effectively
|
## 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
|
## This should be called on a connected socket, and will perform
|
||||||
## an SSL handshake immediately.
|
## an SSL handshake immediately.
|
||||||
##
|
##
|
||||||
|
## FIXME:
|
||||||
## **Disclaimer**: This code is not well tested, may be very unsafe and
|
## **Disclaimer**: This code is not well tested, may be very unsafe and
|
||||||
## prone to security vulnerabilities.
|
## prone to security vulnerabilities.
|
||||||
wrapSocket(ctx, socket)
|
wrapSocket(ctx, socket)
|
||||||
|
|
@ -682,6 +740,9 @@ when defineSsl:
|
||||||
discard SSL_set_tlsext_host_name(socket.sslHandle, hostname)
|
discard SSL_set_tlsext_host_name(socket.sslHandle, hostname)
|
||||||
let ret = SSL_connect(socket.sslHandle)
|
let ret = SSL_connect(socket.sslHandle)
|
||||||
socketError(socket, ret)
|
socketError(socket, ret)
|
||||||
|
when not defined(nimDisableCertificateValidation) and not defined(windows):
|
||||||
|
if hostname.len > 0 and not isIpAddress(hostname):
|
||||||
|
socket.checkCertName(hostname)
|
||||||
of handshakeAsServer:
|
of handshakeAsServer:
|
||||||
let ret = SSL_accept(socket.sslHandle)
|
let ret = SSL_accept(socket.sslHandle)
|
||||||
socketError(socket, ret)
|
socketError(socket, ret)
|
||||||
|
|
@ -1638,6 +1699,9 @@ proc connect*(socket: Socket, address: string,
|
||||||
|
|
||||||
let ret = SSL_connect(socket.sslHandle)
|
let ret = SSL_connect(socket.sslHandle)
|
||||||
socketError(socket, ret)
|
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),
|
proc connectAsync(socket: Socket, name: string, port = Port(0),
|
||||||
af: Domain = AF_INET) {.tags: [ReadIOEffect].} =
|
af: Domain = AF_INET) {.tags: [ReadIOEffect].} =
|
||||||
|
|
|
||||||
97
lib/pure/ssl_certs.nim
Normal file
97
lib/pure/ssl_certs.nim
Normal file
|
|
@ -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.}
|
||||||
|
|
@ -647,3 +647,74 @@ proc md5_Str*(str: string): string =
|
||||||
|
|
||||||
when defined(nimHasStyleChecks):
|
when defined(nimHasStyleChecks):
|
||||||
{.pop.}
|
{.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
|
||||||
|
|
|
||||||
126
tests/stdlib/thttpclient_ssl.nim
Normal file
126
tests/stdlib/thttpclient_ssl.nim
Normal file
|
|
@ -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"))
|
||||||
29
tests/stdlib/thttpclient_ssl_cert.pem
Normal file
29
tests/stdlib/thttpclient_ssl_cert.pem
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
52
tests/stdlib/thttpclient_ssl_key.pem
Normal file
52
tests/stdlib/thttpclient_ssl_key.pem
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
6THbQsJpR5AalTVvXs3X1RnCLnHiNYg=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
203
tests/untestable/thttpclient_ssl.nim
Normal file
203
tests/untestable/thttpclient_ssl.nim
Normal file
|
|
@ -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)
|
||||||
40
tests/untestable/thttpclient_ssl_disabled.nim
Normal file
40
tests/untestable/thttpclient_ssl_disabled.nim
Normal file
|
|
@ -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
|
||||||
74
tests/untestable/thttpclient_ssl_env_var.nim
Normal file
74
tests/untestable/thttpclient_ssl_env_var.nim
Normal file
|
|
@ -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."
|
||||||
Loading…
Add table
Add a link
Reference in a new issue