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

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].} =