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

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