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:
Federico Ceratto 2020-03-20 16:11:39 +00:00 • committed by GitHub
commit 5b85444244
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 872 additions and 12 deletions

91
.github/workflows/ci_ssl.yml vendored Normal file
View 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

View file

@ -29,6 +29,8 @@ install:
- cd ..
build_script:
- openssl version
- openssl version -d
- bin\nim c koch
- koch runCI

View file

@ -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

View file

@ -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 <ssl_certs.html>`_
## module are scanned to locate CA certificates.
##
## See `newContext <net.html#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.

View file

@ -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 <ssl_certs.html>`_ 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].} =

97
lib/pure/ssl_certs.nim Normal file
View 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.}

View file

@ -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

View 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"))

View file

@ -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-----

View file

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
6THbQsJpR5AalTVvXs3X1RnCLnHiNYg=
-----END PRIVATE KEY-----

View 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)

View 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

View 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."