From 7c48e9e150bfdfa45cdce5429a5903f9588f949f Mon Sep 17 00:00:00 2001 From: aeris Date: Wed, 6 May 2020 16:11:05 +0200 Subject: [PATCH] Support OpenSSL 1.1 and so TLSv1.3 --- Dockerfile | 37 +++-- Makefile | 159 ++++++++++++--------- bin/bundle | 118 +++++++++++++++- bin/console | 7 + bin/rspec | 31 ++++- bin/test | 6 +- bin/tls_server.rb | 174 ++++++++++++----------- cryptcheck.gemspec | 68 +++++---- lib/cryptcheck.rb | 5 +- lib/cryptcheck/fixture.rb | 52 ------- lib/cryptcheck/state.rb | 1 - lib/cryptcheck/tls.rb | 2 + lib/cryptcheck/tls/cipher.rb | 64 +++++---- lib/cryptcheck/tls/curve.rb | 9 +- lib/cryptcheck/tls/engine.rb | 57 ++++---- lib/cryptcheck/tls/fixture.rb | 185 ------------------------- lib/cryptcheck/tls/method.rb | 9 +- lib/fixtures/00_openssl.rb | 23 +++ lib/fixtures/01_openssl/certificate.rb | 11 ++ lib/fixtures/01_openssl/context.rb | 71 ++++++++++ lib/fixtures/01_openssl/dh.rb | 46 ++++++ lib/fixtures/01_openssl/dsa.rb | 35 +++++ lib/fixtures/01_openssl/ec.rb | 50 +++++++ lib/fixtures/01_openssl/pkey.rb | 47 +++++++ lib/fixtures/01_openssl/rsa.rb | 44 ++++++ lib/fixtures/01_openssl/store.rb | 27 ++++ lib/fixtures/integer.rb | 20 +++ lib/fixtures/string.rb | 20 +++ lib/fixtures/tls.rb | 0 set-env | 23 ++- 30 files changed, 895 insertions(+), 506 deletions(-) create mode 100755 bin/console mode change 120000 => 100755 bin/test delete mode 100644 lib/cryptcheck/fixture.rb delete mode 100644 lib/cryptcheck/tls/fixture.rb create mode 100644 lib/fixtures/00_openssl.rb create mode 100644 lib/fixtures/01_openssl/certificate.rb create mode 100644 lib/fixtures/01_openssl/context.rb create mode 100644 lib/fixtures/01_openssl/dh.rb create mode 100644 lib/fixtures/01_openssl/dsa.rb create mode 100644 lib/fixtures/01_openssl/ec.rb create mode 100644 lib/fixtures/01_openssl/pkey.rb create mode 100644 lib/fixtures/01_openssl/rsa.rb create mode 100644 lib/fixtures/01_openssl/store.rb create mode 100644 lib/fixtures/integer.rb create mode 100644 lib/fixtures/string.rb create mode 100644 lib/fixtures/tls.rb diff --git a/Dockerfile b/Dockerfile index d9649d1..b37be4a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,11 @@ -FROM alpine:3.10 AS builder +FROM alpine:3.11 AS builder MAINTAINER aeris +ARG OPENSSL_VERSION=1.0.2j +ARG OPENSSL_BINDING=1.0 +ARG OPENSSL_LIB_VERSION=1.0.0 +ARG RUBY_VERSION=2.3.8-cryptcheck + RUN apk add --update make gcc \ linux-headers readline-dev libxml2-dev yaml-dev zlib-dev libffi-dev gdbm-dev ncurses-dev \ ca-certificates wget patch perl musl-dev bash coreutils git @@ -8,35 +13,37 @@ RUN apk add --update make gcc \ ENV PATH /usr/local/rbenv/shims:/usr/local/rbenv/bin:$PATH ENV RBENV_ROOT /usr/local/rbenv ENV RUBY_CONFIGURE_OPTS --disable-install-doc - -ENV C_INCLUDE_PATH /cryptcheck/build/openssl/include -ENV CPLUS_INCLUDE_PATH /cryptcheck/build/openssl/include -ENV LIBRARY_PATH /cryptcheck/lib ENV LD_LIBRARY_PATH /cryptcheck/lib -RUN git clone https://github.com/rbenv/rbenv "$RBENV_ROOT" -b v1.1.2 --depth 1 -RUN git clone https://github.com/sstephenson/ruby-build "$RBENV_ROOT/plugins/ruby-build" - WORKDIR /cryptcheck/ COPY . /cryptcheck/ -RUN make libs -RUN make rbenv -RUN echo "gem: --no-test --no-document" > /etc/gemrc && \ +RUN make openssl-$OPENSSL_BINDING rbenv ruby-$OPENSSL_BINDING && \ + cp build/openssl-$OPENSSL_VERSION/libssl.so \ + build/openssl-$OPENSSL_VERSION/libssl.so.$OPENSSL_LIB_VERSION \ + build/openssl-$OPENSSL_VERSION/libcrypto.so \ + build/openssl-$OPENSSL_VERSION/libcrypto.so.$OPENSSL_LIB_VERSION \ + lib && \ + make clean +RUN echo "gem: --no-test --no-document" > /root/.gemrc && \ + rbenv local $RUBY_VERSION && \ gem install bundler && \ - bundle install --deployment --without development test + rm -f Gemfile.lock && bundle update && \ + bundle config set deployment true && \ + bundle config set without 'development test' && \ + bundle install -FROM alpine:3.10 AS engine +FROM alpine:3.11 AS engine MAINTAINER aeris WORKDIR /cryptcheck/ RUN apk add --update tini bash ca-certificates libxml2 yaml zlib libffi gdbm ncurses ENV PATH /usr/local/rbenv/shims:/usr/local/rbenv/bin:$PATH ENV LD_LIBRARY_PATH /cryptcheck/lib +ENV RBENV_ROOT /usr/local/rbenv ENTRYPOINT ["/sbin/tini", "--", "/cryptcheck/bin/cryptcheck"] -COPY --from=builder /etc/gemrc /etc/gemrc +COPY --from=builder /root/.gemrc /root/.gemrc COPY --from=builder /usr/local/rbenv/ /usr/local/rbenv/ COPY --from=builder /cryptcheck/ /cryptcheck/ - diff --git a/Makefile b/Makefile index b1b5cd9..d489af4 100644 --- a/Makefile +++ b/Makefile @@ -1,29 +1,33 @@ -PWD = $(shell pwd) -OPENSSL_LIB_VERSION = 1.0.0 -OPENSSL_VERSION = 1.0.2j -OPENSSL_NAME = openssl-$(OPENSSL_VERSION) -OPENSSL_DIR = build/$(OPENSSL_NAME) -RUBY_MAJOR_VERSION = 2.3 -RUBY_VERSION = $(RUBY_MAJOR_VERSION).8 -RBENV_DIR = $(RBENV_ROOT)/versions/$(RUBY_VERSION)-cryptcheck RBENV_ROOT ?= ~/.rbenv -export LIBRARY_PATH ?= $(PWD)/lib -export C_INCLUDE_PATH ?= $(PWD)/build/openssl/include -export LD_LIBRARY_PATH ?= $(PWD)/lib +RBENV__VERSION := v1.1.2 +RUBY_BUILD_VERSION = v20200401 + +OPENSSL_1_0_VERSION = 1.0.2j +OPENSSL_1_1_VERSION = 1.1.1g + +RUBY_1_0_VERSION = 2.3.8 +RUBY_1_1_VERSION = 2.6.6 + +ROOT_DIR = $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) +BUILD_DIR = $(ROOT_DIR)/build + +LIBRARY_PATH_1_0 = $(BUILD_DIR)/openssl-$(OPENSSL_1_0_VERSION) +C_INCLUDE_PATH_1_0 = $(LIBRARY_PATH_1_0)/include +LIBRARY_PATH_1_1 = $(BUILD_DIR)/openssl-$(OPENSSL_1_1_VERSION) +C_INCLUDE_PATH_1_1 = $(LIBRARY_PATH_1_1)/include + +MAKE_OPTS ?= -j $(shell nproc) .SECONDARY: -.SUFFIXES: -all: libs rbenv +clean: + rm -rf build/ -clean: clean-libs -clean-libs: - [ -d "build/openssl/" ] \ - && find "build/openssl/" \( -name "*.o" -o -name "*.so" \) -delete \ - || true - rm -f lib/libcrypto.so* lib/libssl.so* "build/openssl//Makefile" -mr-proper: - rm -rf lib/libcrypto.so* lib/libssl.so* lib/openssl.so build +$(RBENV_ROOT)/: + git clone https://github.com/rbenv/rbenv/ "$@" -b "$(RBENV__VERSION)" --depth 1 +$(RBENV_ROOT)/plugins/ruby-build/: | $(RBENV_ROOT)/ + git clone https://github.com/rbenv/ruby-build/ "$@" -b "$(RUBY_BUILD_VERSION)" --depth 1 +rbenv: | $(RBENV_ROOT)/plugins/ruby-build/ build/: mkdir "$@" @@ -31,63 +35,84 @@ build/: build/chacha-poly.patch: | build/ wget -q https://github.com/cloudflare/sslconfig/raw/master/patches/openssl__chacha20_poly1305_draft_and_rfc_ossl102j.patch -O "$@" -build/$(OPENSSL_NAME).tar.gz: | build/ - wget -q "https://www.openssl.org/source/old/1.0.2/$(OPENSSL_NAME).tar.gz" -O "$@" - -build/openssl/: | $(OPENSSL_DIR)/ - ln -s "$(OPENSSL_NAME)" "build/openssl" +build/openssl-%.tar.gz: | build/ + wget -q "https://www.openssl.org/source/$(notdir $@)" -O "$@" -$(OPENSSL_DIR)/: build/$(OPENSSL_NAME).tar.gz build/chacha-poly.patch - tar -C build -xf "build/$(OPENSSL_NAME).tar.gz" - patch -d "$(OPENSSL_DIR)" -p1 < build/chacha-poly.patch +build/openssl-$(OPENSSL_1_0_VERSION)/: build/openssl-$(OPENSSL_1_0_VERSION).tar.gz build/chacha-poly.patch + tar -C build -xf "$<" + patch -d "$@" -p1 < build/chacha-poly.patch for p in patches/openssl/*.patch; do patch -d "$@" -p1 < "$$p"; done -build/openssl/Makefile: | build/openssl/ - cd build/openssl/ && ./config enable-ssl2 enable-ssl3 enable-ssl3-method enable-md2 enable-rc5 enable-weak-ssl-ciphers enable-shared +build/openssl-$(OPENSSL_1_1_VERSION)/: build/openssl-$(OPENSSL_1_1_VERSION).tar.gz build/chacha-poly.patch + tar -C build -xf "$<" -build/openssl/libssl.so \ -build/openssl/libcrypto.so: build/openssl/Makefile - $(MAKE) -C build/openssl/ +.ONESHELL: +build/openssl-%/Makefile: | build/openssl-%/ + cd "$(dir $@)" + ./config --prefix=/usr --openssldir=/etc/ssl \ + enable-ssl2 enable-ssl3 enable-ssl3-method \ + enable-md2 enable-rc5 enable-weak-ssl-ciphers enable-shared + $(MAKE) $(MAKE_OPTS) depend -LIBS = lib/libssl.so lib/libcrypto.so lib/libssl.so.$(OPENSSL_LIB_VERSION) lib/libcrypto.so.$(OPENSSL_LIB_VERSION) -lib/%.so: build/openssl/%.so - cp "$<" "$@" -lib/%.so.$(OPENSSL_LIB_VERSION): lib/%.so - ln -fs "$(notdir $(subst .$(OPENSSL_LIB_VERSION),,$@))" "$@" -libs: $(LIBS) - -$(RBENV_ROOT)/: - git clone https://github.com/rbenv/rbenv/ $@ -b v1.1.1 --depth 1 - -$(RBENV_ROOT)/plugins/ruby-build/: | $(RBENV_ROOT)/ - git clone https://github.com/rbenv/ruby-build/ $@ -b v20171215 --depth 1 +build/openssl-%/libssl.so build/openssl-%/libcrypto.so: build/openssl-%/Makefile + $(MAKE) -C "$(dir $<)" $(MAKE_OPTS) -$(RBENV_ROOT)/plugins/ruby-build/share/ruby-build/$(RUBY_VERSION): | $(RBENV_ROOT)/plugins/ruby-build/ +openssl-1.0: build/openssl-$(OPENSSL_1_0_VERSION)/libssl.so build/openssl-$(OPENSSL_1_0_VERSION)/libcrypto.so +openssl-1.1: build/openssl-$(OPENSSL_1_1_VERSION)/libssl.so build/openssl-$(OPENSSL_1_1_VERSION)/libcrypto.so +openssl: openssl-1.0 openssl-1.1 -build/$(RUBY_VERSION)-cryptcheck: $(RBENV_ROOT)/plugins/ruby-build/share/ruby-build/$(RUBY_VERSION) - cp $< $@ +build/$(RUBY_1_0_VERSION)-cryptcheck: $(RBENV_ROOT)/plugins/ruby-build/share/ruby-build/$(RUBY_1_0_VERSION) + cp "$<" "$@" +build/$(RUBY_1_1_VERSION)-cryptcheck: $(RBENV_ROOT)/plugins/ruby-build/share/ruby-build/$(RUBY_1_1_VERSION) + cp "$<" "$@" -rbenv: build/$(RUBY_VERSION)-cryptcheck $(LIBS) | $(OPENSSL_DIR)/ +$(RBENV_ROOT)/versions/$(RUBY_1_0_VERSION)-cryptcheck: build/$(RUBY_1_0_VERSION)-cryptcheck openssl-1.0 cat patches/ruby/*.patch | \ - RUBY_BUILD_CACHE_PATH=$(PWD)/build \ - RUBY_BUILD_DEFINITIONS=$(PWD)/build \ - MAKE_OPTS="-j $(shell nproc)" rbenv install -fp $(RUBY_VERSION)-cryptcheck - # rbenv sequester $(RUBY_VERSION)-cryptcheck - rbenv local $(RUBY_VERSION)-cryptcheck - gem install bundler - bundle install - -spec/faketime/libfaketime.so: spec/faketime/faketime.c spec/faketime/faketime.h - $(CC) $^ -o $@ -shared -fPIC -ldl -std=c99 -Werror -Wall -lib/libfaketime.so: spec/faketime/libfaketime.so - ln -fs ../$< $@ -faketime: lib/libfaketime.so + LIBRARY_PATH="$(LIBRARY_PATH_1_0)" \ + C_INCLUDE_PATH="$(C_INCLUDE_PATH_1_0)" \ + LD_LIBRARY_PATH="$(LIBRARY_PATH_1_0)" \ + RUBY_BUILD_CACHE_PATH="$(BUILD_DIR)" \ + RUBY_BUILD_DEFINITIONS="$(BUILD_DIR)" \ + MAKE_OPTS="$(MAKE_OPTS)" rbenv install -fp "$(notdir $@)" +$(RBENV_ROOT)/versions/$(RUBY_1_1_VERSION)-cryptcheck: build/$(RUBY_1_1_VERSION)-cryptcheck openssl-1.1 + cat patches/ciphersuites.patch | \ + LIBRARY_PATH="$(LIBRARY_PATH_1_1)" \ + C_INCLUDE_PATH="$(C_INCLUDE_PATH_1_1)" \ + LD_LIBRARY_PATH="$(LIBRARY_PATH_1_1)" \ + RUBY_BUILD_CACHE_PATH="$(BUILD_DIR)" \ + RUBY_BUILD_DEFINITIONS="$(BUILD_DIR)" \ + MAKE_OPTS="$(MAKE_OPTS)" rbenv install -fp "$(notdir $@)" +ruby-1.0: $(RBENV_ROOT)/versions/$(RUBY_1_0_VERSION)-cryptcheck +ruby-1.1: $(RBENV_ROOT)/versions/$(RUBY_1_1_VERSION)-cryptcheck +ruby: ruby-1.0 ruby-1.1 + +build/libfaketime.so: spec/faketime/faketime.c spec/faketime/faketime.h + $(CC) $^ -o "$@" -shared -fPIC -ldl -std=c99 -Werror -Wall +faketime: build/libfaketime.so +.PHONY: faketime test-material: bin/generate-test-material.rb test: spec/faketime/libfaketime.so - bin/rspec - -docker: - docker build . -t aeris22/cryptcheck:v2 -t aeris22/cryptcheck:v2.1 -t aeris22/cryptcheck:latest + LD_LIBRARY_PATH="$(LIBRARY_PATH_1_0):$(BUILD_DIR)" bin/rspec +.PHONY: test + +docker-1.0: + docker build . --target engine \ + -t aeris22/cryptcheck:v2-1.0 \ + -t aeris22/cryptcheck:v2.2-1.0 \ + -t aeris22/cryptcheck:latest-1.0 \ + -t aeris22/cryptcheck:v2 \ + -t aeris22/cryptcheck:v2.2 \ + -t aeris22/cryptcheck:latest +docker-1.1: + docker build . --target engine \ + --build-arg OPENSSL_VERSION=1.1.1g \ + --build-arg OPENSSL_BINDING=1.1 \ + --build-arg OPENSSL_LIB_VERSION=1.1 \ + --build-arg RUBY_VERSION=2.6.6-cryptcheck \ + -t aeris22/cryptcheck:v2-1.1 \ + -t aeris22/cryptcheck:v2.2-1.1 \ + -t aeris22/cryptcheck:latest-1.1 +docker: docker-1.0 docker-1.1 diff --git a/bin/bundle b/bin/bundle index f0241cb..a71368e 100755 --- a/bin/bundle +++ b/bin/bundle @@ -1,4 +1,114 @@ -#!/bin/bash -: ${RBENV_ROOT:=$HOME/.rbenv} -DIR="$(readlink -m "$(dirname "$0")")" -LD_LIBRARY_PATH="${DIR}/../lib" "${RBENV_ROOT}/shims/bundle" $* +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../../Gemfile", __FILE__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_version + @bundler_version ||= + env_var_version || cli_arg_version || + lockfile_version + end + + def bundler_requirement + return "#{Gem::Requirement.default}.a" unless bundler_version + + bundler_gem_version = Gem::Version.new(bundler_version) + + requirement = bundler_gem_version.approximate_recommendation + + return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0") + + requirement += ".a" if bundler_gem_version.prerelease? + + requirement + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..1e604cd --- /dev/null +++ b/bin/console @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +require 'rubygems' +require 'bundler/setup' +require 'pry' +require 'cryptcheck' + +Pry.start diff --git a/bin/rspec b/bin/rspec index d083430..a6c7852 100755 --- a/bin/rspec +++ b/bin/rspec @@ -1,2 +1,29 @@ -#!/bin/bash -LD_PRELOAD=${PWD}/lib/libfaketime.so LD_LIBRARY_PATH=${PWD}/lib bundle exec rspec $@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rspec' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +bundle_binstub = File.expand_path("../bundle", __FILE__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("rspec-core", "rspec") diff --git a/bin/test b/bin/test deleted file mode 120000 index 22882cf..0000000 --- a/bin/test +++ /dev/null @@ -1 +0,0 @@ -runner \ No newline at end of file diff --git a/bin/test b/bin/test new file mode 100755 index 0000000..0b50eba --- /dev/null +++ b/bin/test @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +require 'bundler/setup' +require 'cryptcheck' +require 'pry-byebug' +CryptCheck::Tls::Https.analyze 'localhost', 443 diff --git a/bin/tls_server.rb b/bin/tls_server.rb index f06cc44..e6c0213 100755 --- a/bin/tls_server.rb +++ b/bin/tls_server.rb @@ -28,102 +28,112 @@ OpenSSL::PKey::EC.send :alias_method, :private?, :private_key? # exit def certificate(key) - CryptCheck::Logger.info 'Generating certificate' - cert = OpenSSL::X509::Certificate.new - cert.version = 2 - cert.serial = rand 2**(20*8-1) .. 2**(20*8) - cert.not_before = Time.now - cert.not_after = Time.now + 365*24*60*60 - - cert.public_key = case key - when OpenSSL::PKey::EC - curve = key.group.curve_name - public = OpenSSL::PKey::EC.new curve - public.public_key = key.public_key - public - else - key.public_key - end - - name = OpenSSL::X509::Name.parse 'CN=localhost' - cert.subject = name - cert.issuer = name - - extension_factory = OpenSSL::X509::ExtensionFactory.new nil, cert - extension_factory.subject_certificate = cert - extension_factory.issuer_certificate = cert - - cert.add_extension extension_factory.create_extension 'basicConstraints', 'CA:TRUE', true - cert.add_extension extension_factory.create_extension 'keyUsage', 'keyEncipherment, dataEncipherment, digitalSignature,nonRepudiation,keyCertSign' - cert.add_extension extension_factory.create_extension 'extendedKeyUsage', 'serverAuth, clientAuth' - cert.add_extension extension_factory.create_extension 'subjectKeyIdentifier', 'hash' - cert.add_extension extension_factory.create_extension 'authorityKeyIdentifier', 'keyid:always' - cert.add_extension extension_factory.create_extension 'subjectAltName', 'DNS:localhost' - - cert.add_extension OpenSSL::X509::Extension.new '1.3.6.1.5.5.7.1.24', '0', true - - cert.sign key, OpenSSL::Digest::SHA512.new - CryptCheck::Logger.info 'Certificate generated' - cert + CryptCheck::Logger.info 'Generating certificate' + cert = OpenSSL::X509::Certificate.new + cert.version = 2 + cert.serial = rand 2 ** (20 * 8 - 1)..2 ** (20 * 8) + cert.not_before = Time.now + cert.not_after = Time.now + 365 * 24 * 60 * 60 + + cert.public_key = case key + when OpenSSL::PKey::EC + curve = key.group.curve_name + public = OpenSSL::PKey::EC.new curve + public.public_key = key.public_key + + public + else + key.public_key + end + + name = OpenSSL::X509::Name.parse 'CN=localhost' + cert.subject = name + cert.issuer = name + + extension_factory = OpenSSL::X509::ExtensionFactory.new nil, cert + extension_factory.subject_certificate = cert + extension_factory.issuer_certificate = cert + + cert.add_extension extension_factory.create_extension 'basicConstraints', 'CA:TRUE', true + cert.add_extension extension_factory.create_extension 'keyUsage', 'keyEncipherment, dataEncipherment, digitalSignature,nonRepudiation,keyCertSign' + cert.add_extension extension_factory.create_extension 'extendedKeyUsage', 'serverAuth, clientAuth' + cert.add_extension extension_factory.create_extension 'subjectKeyIdentifier', 'hash' + cert.add_extension extension_factory.create_extension 'authorityKeyIdentifier', 'keyid:always' + cert.add_extension extension_factory.create_extension 'subjectAltName', 'DNS:localhost' + + cert.add_extension OpenSSL::X509::Extension.new '1.3.6.1.5.5.7.1.24', '0', true + + cert.sign key, OpenSSL::Digest::SHA512.new + CryptCheck::Logger.info 'Certificate generated' + cert end -key = OpenSSL::PKey::RSA.new File.read 'config/rsa-2048.pem' -# key = OpenSSL::PKey::EC.new('prime256v1').generate_key -cert = certificate key +rsa_key = OpenSSL::PKey::RSA.new File.read 'config/rsa-2048.pem' +rsa_cert = certificate rsa_key +ec_key = OpenSSL::PKey::EC.new('prime256v1').generate_key +ec_cert = certificate ec_key CryptCheck::Logger.info 'Starting server' context = OpenSSL::SSL::SSLContext.new #context = OpenSSL::SSL::SSLContext.new :SSLv3 #context = OpenSSL::SSL::SSLContext.new :TLSv1_1 -context.cert = cert -context.key = key -context.ciphers = ARGV[0] || 'EECDH+AESGCM' - -#dh = OpenSSL::PKey::DH.new File.read 'config/dh-4096.pem' -#context.tmp_dh_callback = proc { dh } -#context.ecdh_curves = CryptCheck::Tls::Server::SUPPORTED_CURVES.join ':' -#context.ecdh_curves = 'secp384r1:secp521r1:sect571r1' -#context.ecdh_curves = 'prime256v1' -#ecdh = OpenSSL::PKey::EC.new('secp384r1').generate_key -#context.tmp_ecdh_callback = proc { ecdh } + +if context.respond_to? :add_certificate + context.add_certificate ec_cert, ec_key + context.add_certificate rsa_cert, rsa_key +else + context.certs = [ec_cert, rsa_cert] + context.keys = [ec_key, rsa_key] +end +ciphers = ARGV[0] || 'EECDH+AESGCM' +puts ciphers +context.ciphers = ciphers + +dh = OpenSSL::PKey::DH.new File.read 'config/dh-2048.pem' +# context.tmp_dh_callback = proc { dh } +# context.ecdh_curves = CryptCheck::Tls::Server::SUPPORTED_CURVES.join ':' +# context.ecdh_curves = 'prime256v1:secp384r1:secp521r1:sect571r1' +# context.ecdh_curves = 'prime256v1' +# ecdh = OpenSSL::PKey::EC.new('prime256v1').generate_key +# context.tmp_ecdh_callback = proc { ecdh } host, port = '::', 5000 -tcp_server = TCPServer.new host, port -tls_server = OpenSSL::SSL::SSLServer.new tcp_server, context +tcp_server = TCPServer.new host, port +tls_server = OpenSSL::SSL::SSLServer.new tcp_server, context ::CryptCheck::Logger.info "Server started on #{host}:#{port}" # ::CryptCheck::Logger.info "Supported ciphers:" # context.ciphers.each { |c| ::CryptCheck::Logger.info c.first } loop do - begin - connection = tls_server.accept - - method = connection.ssl_version - - dh = connection.tmp_key - cipher = connection.cipher - cipher = CryptCheck::Tls::Cipher.new method, cipher.first - states = cipher.states - # p states - # text = %i(critical error warning good perfect best).collect do |s| - # states[s].collect { |t| t.to_s.colorize s }.join ' ' - # end.reject &:empty? - # text = [] - # text = text.join ' ' - # text = '' - - dh = dh ? " (#{'PFS'.colorize :good} : #{CryptCheck::Tls.key_to_s dh})" : '' - CryptCheck::Logger.info { "#{cipher}#{dh}" } - - data = connection.gets - if data - CryptCheck::Logger.info data - end - connection.puts 'HTTP/1.1 200 OK' - connection.puts 'Strict-Transport-Security: max-age=31536000' - connection.close - rescue OpenSSL::SSL::SSLError, SystemCallError - end + begin + connection = tls_server.accept + + method = connection.ssl_version + + dh = connection.tmp_key + cipher = connection.cipher + cipher = CryptCheck::Tls::Cipher.new method, cipher.first + states = cipher.states + # p states + # text = %i(critical error warning good perfect best).collect do |s| + # states[s].collect { |t| t.to_s.colorize s }.join ' ' + # end.reject &:empty? + # text = [] + # text = text.join ' ' + # text = '' + + dh = dh ? " (#{'PFS'.colorize :good} : #{CryptCheck::Tls.key_to_s dh})" : '' + CryptCheck::Logger.info { "#{cipher}#{dh}" } + + data = connection.gets + if data + CryptCheck::Logger.info data + end + connection.puts 'HTTP/1.1 200 OK' + connection.puts 'Strict-Transport-Security: max-age=31536000' + connection.close + rescue OpenSSL::SSL::SSLError, SystemCallError + end end diff --git a/cryptcheck.gemspec b/cryptcheck.gemspec index 3b58805..d7e2422 100644 --- a/cryptcheck.gemspec +++ b/cryptcheck.gemspec @@ -3,42 +3,50 @@ lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) Gem::Specification.new do |spec| - spec.name = 'cryptcheck' - spec.version = '2.0.0' - spec.authors = ['Aeris'] - spec.email = ['aeris+tls@imirhil.fr'] + spec.name = 'cryptcheck' + spec.version = '2.0.0' + spec.authors = ['Aeris'] + spec.email = ['aeris+tls@imirhil.fr'] - spec.summary = %q{Check best practices on crypto-stack implementation} - spec.description = %q{Verify if best practices are well implemented on current crypto-stack (TLS & SSH) protocol (HTTPS, SMTP, XMPP, SSH & VPN)} - spec.homepage = 'https://tls.imirhil.fr' - spec.license = 'AGPL-3.0+' + spec.summary = %q{Check best practices on crypto-stack implementation} + spec.description = %q{Verify if best practices are well implemented on current crypto-stack (TLS & SSH) protocol (HTTPS, SMTP, XMPP, SSH & VPN)} + spec.homepage = 'https://tls.imirhil.fr' + spec.license = 'AGPL-3.0+' - if spec.respond_to?(:metadata) - spec.metadata['allowed_push_host'] = 'TODO: Set to "http://mygemserver.com"' - else - raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.' - end + if spec.respond_to?(:metadata) + spec.metadata['allowed_push_host'] = 'TODO: Set to "http://mygemserver.com"' + else + raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.' + end - spec.files = { '*.rb' => %w(lib) } - .collect_concat { |e, ds| ds.collect_concat { |d| Dir[File.join d, '**', e] } } + spec.files = { '*.rb' => %w(lib) }.collect_concat do |e, ds| + ds.collect_concat do |d| + Dir[File.join d, '**', e] + end + end # spec.bindir = 'bin' # spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } # spec.test_files = spec.files.grep(%r{^spec/}) - spec.require_paths = %w(lib) + spec.require_paths = %w(lib) - spec.add_development_dependency 'bundler' - spec.add_development_dependency 'rake' - spec.add_development_dependency 'rspec' - spec.add_development_dependency 'ffi' - spec.add_development_dependency 'pry-byebug' - spec.add_development_dependency 'pry-rescue' - spec.add_development_dependency 'pry-stack_explorer' + spec.add_development_dependency 'bundler' + spec.add_development_dependency 'rake' + spec.add_development_dependency 'rspec' + spec.add_development_dependency 'ffi' + if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.4') + # pry-byebug supports 2.3 only up to 3.6.x + spec.add_development_dependency 'pry-byebug', '~> 3.6.0' + # and there is a bug if using more than pry 0.12.x + spec.add_development_dependency 'pry', '~> 0.12.2' + else + spec.add_development_dependency 'pry-byebug' + end - spec.add_dependency 'httparty' - spec.add_dependency 'nokogiri' - spec.add_dependency 'parallel' - spec.add_dependency 'ruby-progressbar' - spec.add_dependency 'colorize' - spec.add_dependency 'awesome_print' - spec.add_dependency 'thor' + spec.add_dependency 'httparty' + spec.add_dependency 'nokogiri' + spec.add_dependency 'parallel' + spec.add_dependency 'ruby-progressbar' + spec.add_dependency 'colorize' + spec.add_dependency 'awesome_print' + spec.add_dependency 'thor' end diff --git a/lib/cryptcheck.rb b/lib/cryptcheck.rb index f124aee..7c20b09 100644 --- a/lib/cryptcheck.rb +++ b/lib/cryptcheck.rb @@ -2,6 +2,7 @@ require 'colorize' require 'ipaddr' require 'timeout' require 'yaml' +require 'openssl' module CryptCheck autoload :State, 'cryptcheck/state' @@ -55,5 +56,5 @@ module CryptCheck end end -require 'cryptcheck/fixture' -require 'cryptcheck/tls/fixture' +fixtures = File.join __dir__, 'fixtures', '**', '*.rb' +Dir[fixtures].sort.each { |f| require f } diff --git a/lib/cryptcheck/fixture.rb b/lib/cryptcheck/fixture.rb deleted file mode 100644 index 7e9005f..0000000 --- a/lib/cryptcheck/fixture.rb +++ /dev/null @@ -1,52 +0,0 @@ -class String - alias :colorize_old :colorize - - COLORS = { - critical: { color: :white, background: :red }, - error: :red, - warning: :light_red, - good: :green, - great: :blue, - best: :magenta, - unknown: { background: :black } - } - - def colorize(state) - color = COLORS[state] || state - self.colorize_old color - end -end - -class Exception - BACKTRACE_REGEXP = /^(.*):(\d+):in `(.*)'$/ - - def colorize - $stderr.puts self.message.colorize(:red) - self.backtrace.each do |line| - line = BACKTRACE_REGEXP.match line - line = '%s:%s:in `%s\'' % [ - line[1].colorize(:yellow), - line[2].colorize(:blue), - line[3].colorize(:magenta) - ] - $stderr.puts line - end - end -end - -class Integer - def humanize - secs = self - [[60, :second], - [60, :minute], - [24, :hour], - [30, :day], - [12, :month]].map do |count, name| - if secs > 0 - secs, n = secs.divmod count - n = n.to_i - n > 0 ? "#{n} #{name}#{n > 1 ? 's' : ''}" : nil - end - end.compact.reverse.join ' ' - end -end diff --git a/lib/cryptcheck/state.rb b/lib/cryptcheck/state.rb index b1ac68c..b2e8cc8 100644 --- a/lib/cryptcheck/state.rb +++ b/lib/cryptcheck/state.rb @@ -99,7 +99,6 @@ module CryptCheck b <=> a end - protected def checks @checks ||= self.available_checks.collect { |c| perform_check c }.flatten(1) + children.collect(&:checks).flatten(1) end diff --git a/lib/cryptcheck/tls.rb b/lib/cryptcheck/tls.rb index 542fd24..1932420 100644 --- a/lib/cryptcheck/tls.rb +++ b/lib/cryptcheck/tls.rb @@ -15,6 +15,8 @@ module CryptCheck def self.key_to_s(key) size, color = case key.type + when :x25519 + ["#{key.curve} #{key.size}", :good] when :ecc ["#{key.group.curve_name} #{key.size}", :good] when :rsa diff --git a/lib/cryptcheck/tls/cipher.rb b/lib/cryptcheck/tls/cipher.rb index 357fa90..32cda3c 100644 --- a/lib/cryptcheck/tls/cipher.rb +++ b/lib/cryptcheck/tls/cipher.rb @@ -25,17 +25,21 @@ module CryptCheck rc4: %w(RC4), des: %w(DES-CBC), des3: %w(3DES DES-CBC3), - aes: %w(AES(128|256) AES-(128|256)), - aes128: %w(AES128 AES-128), - aes256: %w(AES256 AES-256), + aes: %w(AES(128|256) AES-(128|256) AES_(128|256)), + aes128: %w(AES128 AES-128 AES_128), + aes256: %w(AES256 AES-256 AES_256), camellia: %w(CAMELLIA(128|256)), seed: %w(SEED), idea: %w(IDEA), chacha20: %w(CHACHA20), + aria: %w(ARIA(128|256) ARIA-(128|256) ARIA_(128|256)), + aria128: %w(ARIA128 ARIA-128 ARIA_128), + aria256: %w(ARIA256 ARIA-256 ARIA_256), # cbc: %w(CBC), - gcm: %w(GCM), - ccm: %w(CCM) + gcm: %w(GCM), + ccm: %w(CCM CCM8), + ccm8: %w(CCM8) }.freeze attr_reader :method, :name @@ -59,23 +63,14 @@ module CryptCheck TYPES.each do |name, ciphers| class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def self.#{name}?(cipher) - #{ciphers}.any? { |c| /(^|-)#\{c\}(-|$)/ =~ cipher } + #{ciphers}.any? { |c| /(^|[-_])#\{c\}([-_]|$)/ =~ cipher } end def #{name}? - #{ciphers}.any? { |c| /(^|-)#\{c\}(-|$)/ =~ @name } + #{ciphers}.any? { |c| /(^|[-_])#\{c\}([-_]|$)/ =~ @name } end RUBY_EVAL end - def self.aes?(cipher) - aes?(cipher) or aes?(cipher) - end - - def aes? - aes128? or aes256? - end - - def self.cbc?(cipher) !aead? cipher end @@ -101,6 +96,7 @@ module CryptCheck end def pfs? + return true if self.method == :TLSv1_3 dhe? or ecdhe? end @@ -194,7 +190,11 @@ module CryptCheck when aes128? [:aes, 128, 128, self.mode] when aes256? - [:aes, 256, 256, self.mode] + [:aes, 256, 128, self.mode] + when aria128? + [:aria, 128, 128, self.mode] + when aria256? + [:aria, 256, 128, self.mode] when camellia? [:camellia, 128, 128, self.mode] when seed? @@ -211,6 +211,8 @@ module CryptCheck [:rc2, 64, 64, self.mode] when null? [nil, 0, 0, nil] + else + raise "Unknown encryption #{@method} #{@name}" end end @@ -218,8 +220,6 @@ module CryptCheck case when gcm? :gcm - when ccm? - :ccm when chacha20? :aead when rc4? @@ -233,6 +233,10 @@ module CryptCheck case when poly1305? [:poly1305, 128] + when ccm8? + [:ccm, 64] + when ccm? + [:ccm, 128] when sha384? [:sha384, 384] when sha256? @@ -275,11 +279,23 @@ module CryptCheck @name <=> other.name end - ALL = 'ALL:COMPLEMENTOFALL'.freeze - SUPPORTED = Method.collect do |m| - context = ::OpenSSL::SSL::SSLContext.new m.to_sym - context.ciphers = ALL - ciphers = context.ciphers.collect { |c| Cipher.new m, c.first } + ALL = 'ALL:COMPLEMENTOFALL'.freeze + ALL_TLSv1_3 = %w[ + TLS_CHACHA20_POLY1305_SHA256 + TLS_AES_128_GCM_SHA256 + TLS_AES_256_GCM_SHA384 + TLS_AES_128_CCM_SHA256 + TLS_AES_128_CCM_8_SHA256 + ].freeze + SUPPORTED = Method.collect do |m| + sym = m.to_sym + ciphers = if sym == :TLSv1_3 + ALL_TLSv1_3.collect { |c| Cipher.new m, c } + else + context = ::OpenSSL::SSL::SSLContext.new sym + context.ciphers = ALL + context.ciphers.collect { |c| Cipher.new m, c.first } + end [m, ciphers.sort] end.to_h.freeze end diff --git a/lib/cryptcheck/tls/curve.rb b/lib/cryptcheck/tls/curve.rb index c3e3c45..cd4b44f 100644 --- a/lib/cryptcheck/tls/curve.rb +++ b/lib/cryptcheck/tls/curve.rb @@ -8,17 +8,10 @@ module CryptCheck @name = name end - # SUPPORTED = %i(sect163k1 sect163r1 sect163r2 sect193r1 - # sect193r2 sect233k1 sect233r1 sect239k1 sect283k1 sect283r1 - # sect409k1 sect409r1 sect571k1 sect571r1 secp160k1 secp160r1 - # secp160r2 secp192k1 secp192r1 secp224k1 secp224r1 secp256k1 - # secp256r1 secp384r1 secp521r1 - # prime256v1 - # brainpoolP256r1 brainpoolP384r1 brainpoolP512r1) SUPPORTED = %i(secp256k1 sect283k1 sect283r1 secp384r1 sect409k1 sect409r1 secp521r1 sect571k1 sect571r1 prime192v1 prime256v1 - brainpoolP256r1 brainpoolP384r1 brainpoolP512r1).collect { |c| self.new c }.freeze + brainpoolP256r1 brainpoolP384r1 brainpoolP512r1 x25519).collect { |c| self.new c }.freeze extend Enumerable diff --git a/lib/cryptcheck/tls/engine.rb b/lib/cryptcheck/tls/engine.rb index 635235b..1360a6c 100644 --- a/lib/cryptcheck/tls/engine.rb +++ b/lib/cryptcheck/tls/engine.rb @@ -10,17 +10,22 @@ module CryptCheck class TLSException < ::StandardError end + class TLSNotAvailableException < TLSException def to_s 'TLS seems not supported on this server' end end + class MethodNotAvailable < TLSException end + class CipherNotAvailable < TLSException end + class InappropriateFallback < TLSException end + class Timeout < ::StandardError def initialize(ip, port) @message = "Timeout when connecting to #{ip}:#{port} (max #{TCP_TIMEOUT.humanize})" @@ -30,11 +35,13 @@ module CryptCheck @message end end + class TLSTimeout < Timeout def initialize(ip, port) @message = "Timeout when TLS connecting to #{ip}:#{port} (max #{TLS_TIMEOUT.humanize})" end end + class ConnectionError < ::StandardError end @@ -86,9 +93,7 @@ module CryptCheck connection = ssl_client method, cipher Logger.info { " Cipher #{cipher}" } dh = connection.tmp_key - if dh - Logger.info { " PFS : #{dh}" } - end + Logger.info { " PFS : #{dh}" } if dh connection rescue TLSException Logger.debug { " Cipher #{cipher} : not supported" } @@ -285,7 +290,7 @@ module CryptCheck ssl_client method, fallback: true rescue InappropriateFallback, CipherNotAvailable, # Seems some servers reply with "sslv3 alert handshake failure"… - MethodNotAvailable, # Seems some servers reply with "wrong version number"… + MethodNotAvailable # Seems some servers reply with "wrong version number"… @fallback_scsv = true end else @@ -349,17 +354,13 @@ module CryptCheck def ssl_connect(socket, context, method, &block) ssl_socket = ::OpenSSL::SSL::SSLSocket.new socket, context ssl_socket.hostname = @hostname if @hostname and method != :SSLv2 - #Logger.trace { "SSL connecting to #{name}" } begin ssl_socket.connect_nonblock - #Logger.trace { "SSL connected to #{name}" } return block_given? ? block.call(ssl_socket) : nil rescue ::OpenSSL::SSL::SSLErrorWaitReadable - #Logger.trace { "Waiting for SSL read to #{name}" } raise TLSTimeout.new(@ip, @port) unless IO.select [ssl_socket], nil, nil, TLS_TIMEOUT retry rescue ::OpenSSL::SSL::SSLErrorWaitWritable - #Logger.trace { "Waiting for SSL write to #{name}" } raise TLSTimeout.new(@ip, @port) unless IO.select nil, [ssl_socket], nil, TLS_TIMEOUT retry rescue ::OpenSSL::SSL::SSLError => e @@ -368,15 +369,18 @@ module CryptCheck /state=SSLv3 read server hello A$/, /state=SSLv3 read server hello A: wrong version number$/, /state=SSLv3 read server hello A: tlsv1 alert protocol version$/, - /state=SSLv3 read server key exchange A: sslv3 alert handshake failure$/ + /state=SSLv3 read server key exchange A: sslv3 alert handshake failure$/, + /state=error: tlsv1 alert protocol version$/ raise MethodNotAvailable, e when /state=SSLv2 read server hello A: peer error no cipher$/, /state=error: no ciphers available$/, /state=SSLv3 read server hello A: sslv3 alert handshake failure$/, /state=error: missing export tmp dh key$/, - /state=error: wrong curve$/ + /state=error: wrong curve$/, + /error: sslv3 alert handshake failure$/ raise CipherNotAvailable, e - when /state=SSLv3 read server hello A: tlsv1 alert inappropriate fallback$/ + when /state=SSLv3 read server hello A: tlsv1 alert inappropriate fallback$/, + /state=error: tlsv1 alert inappropriate fallback$/ raise InappropriateFallback, e end raise @@ -393,16 +397,18 @@ module CryptCheck def ssl_client(method, ciphers = nil, curves: nil, fallback: false, &block) sleep SLOW_DOWN if SLOW_DOWN > 0 - ssl_context = ::OpenSSL::SSL::SSLContext.new method.to_sym + method = method.to_sym + ssl_context = ::OpenSSL::SSL::SSLContext.new method ssl_context.enable_fallback_scsv if fallback - if ciphers - ciphers = [ciphers] unless ciphers.is_a? Enumerable - ciphers = ciphers.collect(&:name).join ':' + ciphers = Array(ciphers).collect(&:name).join ':' if ciphers + + if method == :TLSv1_3 + ssl_context.ciphersuites = ciphers if ciphers else - ciphers = Cipher::ALL + ciphers ||= Cipher::ALL + ssl_context.ciphers = ciphers end - ssl_context.ciphers = ciphers if curves curves = [curves] unless curves.is_a? Enumerable @@ -413,16 +419,17 @@ module CryptCheck end Logger.trace { "Try method=#{method} / ciphers=#{ciphers} / curves=#{curves} / scsv=#{fallback}" } - begin - connect do |socket| - ssl_connect socket, ssl_context, method do |ssl_socket| - return block_given? ? block.call(ssl_socket) : ssl_socket - end + connect do |socket| + ssl_connect socket, ssl_context, method do |ssl_socket| + return block_given? ? block.call(ssl_socket) : ssl_socket end - rescue => e - Logger.trace { "Error occurs : #{e}" } - raise end + rescue TLSException => e + Logger.trace { "Error occurs : #{e}" } + raise + rescue => e + Logger.trace { "Error occurs : #{e}" } + raise TLSException.new e end def verify_certs diff --git a/lib/cryptcheck/tls/fixture.rb b/lib/cryptcheck/tls/fixture.rb deleted file mode 100644 index fd6ffce..0000000 --- a/lib/cryptcheck/tls/fixture.rb +++ /dev/null @@ -1,185 +0,0 @@ -require 'openssl' - -class ::OpenSSL::PKey::PKey - def fingerprint - ::OpenSSL::Digest::SHA256.hexdigest self.to_der - end -end - -class ::OpenSSL::PKey::EC - def type - :ecc - end - - def size - self.group.degree - end - - def curve - self.group.curve_name - end - - def to_s - "ECC #{self.size} bits" - end - - def to_h - { type: :ecc, curve: self.curve, size: self.size, fingerprint: self.fingerprint, states: self.states } - end - - protected - include ::CryptCheck::State - - CHECKS = [ - [:ecc, %i(critical error warning), -> (s) do - case s.size - when 0...160 - :critical - when 160...192 - :error - when 192...256 - :warning - else - false - end - end] - ].freeze - - def available_checks - CHECKS - end -end - -class ::OpenSSL::PKey::RSA - def type - :rsa - end - - def size - self.n.num_bits - end - - def to_s - "RSA #{self.size} bits" - end - - def to_h - { type: :rsa, size: self.size, fingerprint: self.fingerprint, states: self.states } - end - - protected - include ::CryptCheck::State - - CHECKS = [ - [:rsa, %i(critical error), ->(s) do - case s.size - when 0...1024 - :critical - when 1024...2048 - :error - else - false - end - end] - ].freeze - - def available_checks - CHECKS - end -end - -class ::OpenSSL::PKey::DSA - def type - :dsa - end - - def size - self.p.num_bits - end - - def to_s - "DSA #{self.size} bits" - end - - def to_h - { type: :dsa, size: self.size, fingerprint: self.fingerprint, states: self.states } - end - - include ::CryptCheck::State - - CHECKS = [ - [:dsa, :critical, -> (_) { true }] - ].freeze - - protected - def available_checks - CHECKS - end -end - -class ::OpenSSL::PKey::DH - def type - :dh - end - - def size - self.p.num_bits - end - - def to_s - "DH #{self.size} bits" - end - - def to_h - { size: self.size, fingerprint: self.fingerprint, states: self.states } - end - - protected - include ::CryptCheck::State - - CHECKS = [ - [:dh, %i(critical error), -> (s) do - case s.size - when 0...1024 - :critical - when 1024...2048 - :error - else - false - end - end] - ].freeze - - protected - def available_checks - CHECKS - end -end - -class ::OpenSSL::X509::Certificate - def fingerprint - ::OpenSSL::Digest::SHA256.hexdigest self.to_der - end -end - -class ::OpenSSL::X509::Store - def add_chains(chains) - chains = [chains] unless chains.is_a? Enumerable - chains.each do |chain| - case chain - when ::OpenSSL::X509::Certificate - self.add_cert chain - else - next unless File.exists? chain - if File.directory?(chain) - Dir.entries(chain) - .collect { |e| File.join chain, e } - .select { |e| File.file? e } - .each { |f| self.add_file f } - else - self.add_file chain - end - end - end - end -end diff --git a/lib/cryptcheck/tls/method.rb b/lib/cryptcheck/tls/method.rb index b0e2aba..fce9727 100644 --- a/lib/cryptcheck/tls/method.rb +++ b/lib/cryptcheck/tls/method.rb @@ -3,8 +3,9 @@ require 'delegate' module CryptCheck module Tls class Method < SimpleDelegator - EXISTING = %i(TLSv1_2 TLSv1_1 TLSv1 SSLv3 SSLv2).freeze - SUPPORTED = (EXISTING & ::OpenSSL::SSL::SSLContext::METHODS) + EXISTING = %i(TLSv1_3 TLSv1_2 TLSv1_1 TLSv1 SSLv3 SSLv2).freeze + # EXISTING = %i(TLSv1_3) + SUPPORTED = EXISTING.select { |m| ::OpenSSL::SSL::SSLContext.supported? m } .collect { |m| [m, self.new(m)] }.to_h.freeze def self.[](method) @@ -19,9 +20,9 @@ module CryptCheck def to_s colors = case self.to_sym - when *%i(SSLv3 SSLv2) + when :SSLv3, :SSLv2 :critical - when :TLSv1_2 + when :TLSv1_2, :TLSv1_3 :good end super.colorize colors diff --git a/lib/fixtures/00_openssl.rb b/lib/fixtures/00_openssl.rb new file mode 100644 index 0000000..ea0564e --- /dev/null +++ b/lib/fixtures/00_openssl.rb @@ -0,0 +1,23 @@ +module Fixture + module OpenSSL + module ClassMethods + def version + Gem::Version.new ::OpenSSL::VERSION + end + + def ge?(version) + self.version >= Gem::Version.new(version) + end + + def ge_2_1_2? + self.ge? '2.1.2' + end + end + + def self.included(base) + base.extend ClassMethods + end + end +end + +::OpenSSL.include Fixture::OpenSSL diff --git a/lib/fixtures/01_openssl/certificate.rb b/lib/fixtures/01_openssl/certificate.rb new file mode 100644 index 0000000..6a16c5a --- /dev/null +++ b/lib/fixtures/01_openssl/certificate.rb @@ -0,0 +1,11 @@ +module Fixture + module OpenSSL + module Certificate + def fingerprint + ::OpenSSL::Digest::SHA256.hexdigest self.to_der + end + end + end +end + +::OpenSSL::X509::Certificate.include Fixture::OpenSSL::Certificate diff --git a/lib/fixtures/01_openssl/context.rb b/lib/fixtures/01_openssl/context.rb new file mode 100644 index 0000000..e2d1c1b --- /dev/null +++ b/lib/fixtures/01_openssl/context.rb @@ -0,0 +1,71 @@ +module Fixture + module OpenSSL + if ::OpenSSL.ge_2_1_2? + module Context + METHODS = { + TLSv1_3: ::OpenSSL::SSL::TLS1_3_VERSION, + TLSv1_2: ::OpenSSL::SSL::TLS1_2_VERSION, + TLSv1_1: ::OpenSSL::SSL::TLS1_1_VERSION, + TLSv1: ::OpenSSL::SSL::TLS1_VERSION, + SSL_3: ::OpenSSL::SSL::SSL3_VERSION, + SSL_2: ::OpenSSL::SSL::SSL2_VERSION + }.freeze + EXCLUDES = { + TLSv1_3: ::OpenSSL::SSL::OP_NO_TLSv1_3, + TLSv1_2: ::OpenSSL::SSL::OP_NO_TLSv1_2, + TLSv1_1: ::OpenSSL::SSL::OP_NO_TLSv1_1, + TLSv1: ::OpenSSL::SSL::OP_NO_TLSv1, + SSL_3: ::OpenSSL::SSL::OP_NO_SSLv3, + SSL_2: ::OpenSSL::SSL::OP_NO_SSLv2 + }.yield_self do |e| + all = e.values + e.collect do |m, o| + excludes = all - [o] + options = excludes.reduce :| + [m, options] + end.to_h + end.freeze + + module Prepend + def initialize(method = nil) + super() + if method + self.options = EXCLUDES[method] + self.min_version = self.max_version = METHODS[method] + end + end + end + + module ClassMethods + def supported?(method) + return false if %i[SSLv2 SSLv3].include? method + self.new method + true + rescue => e + ap e + false + end + end + + def self.included(base) + base.extend ClassMethods + base.prepend Prepend + end + end + else + module Context + module ClassMethods + def supported?(method) + ::OpenSSL::SSL::SSLContext::METHODS.include? method + end + end + + def self.included(base) + base.extend ClassMethods + end + end + end + end +end + +::OpenSSL::SSL::SSLContext.include Fixture::OpenSSL::Context diff --git a/lib/fixtures/01_openssl/dh.rb b/lib/fixtures/01_openssl/dh.rb new file mode 100644 index 0000000..21c1c9d --- /dev/null +++ b/lib/fixtures/01_openssl/dh.rb @@ -0,0 +1,46 @@ +module Fixture + module OpenSSL + module DH + def type + :dh + end + + def size + self.p.num_bits + end + + def to_s + "DH #{self.size} bits" + end + + def to_h + { size: self.size, fingerprint: self.fingerprint, states: self.states } + end + + protected + + include ::CryptCheck::State + + CHECKS = [ + [:dh, %i(critical error), -> (s) do + case s.size + when 0...1024 + :critical + when 1024...2048 + :error + else + false + end + end] + ].freeze + + protected + + def available_checks + CHECKS + end + end + end +end + +::OpenSSL::PKey::DH.prepend Fixture::OpenSSL::DH diff --git a/lib/fixtures/01_openssl/dsa.rb b/lib/fixtures/01_openssl/dsa.rb new file mode 100644 index 0000000..6b40819 --- /dev/null +++ b/lib/fixtures/01_openssl/dsa.rb @@ -0,0 +1,35 @@ +module Fixture + module OpenSSL + module DSA + def type + :dsa + end + + def size + self.p.num_bits + end + + def to_s + "DSA #{self.size} bits" + end + + def to_h + { type: :dsa, size: self.size, fingerprint: self.fingerprint, states: self.states } + end + + include ::CryptCheck::State + + CHECKS = [ + [:dsa, :critical, -> (_) { true }] + ].freeze + + protected + + def available_checks + CHECKS + end + end + end +end + +::OpenSSL::PKey::DSA.include Fixture::OpenSSL::DSA diff --git a/lib/fixtures/01_openssl/ec.rb b/lib/fixtures/01_openssl/ec.rb new file mode 100644 index 0000000..21dd9fc --- /dev/null +++ b/lib/fixtures/01_openssl/ec.rb @@ -0,0 +1,50 @@ +module Fixture + module OpenSSL + module EC + def type + :ecc + end + + def size + self.group.degree + end + + def curve + self.group.curve_name + end + + def to_s + "ECC #{self.size} bits" + end + + def to_h + { type: :ecc, curve: self.curve, size: self.size, fingerprint: self.fingerprint, states: self.states } + end + + protected + + include ::CryptCheck::State + + CHECKS = [ + [:ecc, %i(critical error warning), -> (s) do + case s.size + when 0...160 + :critical + when 160...192 + :error + when 192...256 + :warning + else + false + end + end] + ].freeze + + def available_checks + CHECKS + end + end + end +end + +::OpenSSL::PKey::EC.include Fixture::OpenSSL::EC diff --git a/lib/fixtures/01_openssl/pkey.rb b/lib/fixtures/01_openssl/pkey.rb new file mode 100644 index 0000000..f2751ca --- /dev/null +++ b/lib/fixtures/01_openssl/pkey.rb @@ -0,0 +1,47 @@ +require 'ostruct' + +module Fixture + module OpenSSL + module PKey + def fingerprint + ::OpenSSL::Digest::SHA256.hexdigest self.to_der + end + + # Currently, Ruby doesn't support curve other than NIST ECC + # For X25519, we got a plain `PKey` instead of an `EC` + # We need to wait for https://github.com/ruby/openssl/pull/329 & + # https://github.com/ruby/openssl/pull/364 for more generic curve support + # So we supposed we have X25519 in case we catch a `PKey` + + def type + :x25519 + end + + def size + 128 + end + + def curve + :x25519 + end + + def to_s + "#{self.size} bits" + end + + def to_h + { type: :ecc, curve: self.curve, size: self.size, fingerprint: self.fingerprint, states: self.states } + end + + include ::CryptCheck::State + + CHECKS = [].freeze + + def available_checks + CHECKS + end + end + end +end + +::OpenSSL::PKey::PKey.include Fixture::OpenSSL::PKey diff --git a/lib/fixtures/01_openssl/rsa.rb b/lib/fixtures/01_openssl/rsa.rb new file mode 100644 index 0000000..1da0e6c --- /dev/null +++ b/lib/fixtures/01_openssl/rsa.rb @@ -0,0 +1,44 @@ +module Fixture + module OpenSSL + module RSA + def type + :rsa + end + + def size + self.n.num_bits + end + + def to_s + "RSA #{self.size} bits" + end + + def to_h + { type: :rsa, size: self.size, fingerprint: self.fingerprint, states: self.states } + end + + protected + + include ::CryptCheck::State + + CHECKS = [ + [:rsa, %i(critical error), ->(s) do + case s.size + when 0...1024 + :critical + when 1024...2048 + :error + else + false + end + end] + ].freeze + + def available_checks + CHECKS + end + end + end +end + +::OpenSSL::PKey::RSA.include Fixture::OpenSSL::RSA diff --git a/lib/fixtures/01_openssl/store.rb b/lib/fixtures/01_openssl/store.rb new file mode 100644 index 0000000..c4911bd --- /dev/null +++ b/lib/fixtures/01_openssl/store.rb @@ -0,0 +1,27 @@ +module Fixture + module OpenSSL + module Store + def add_chains(chains) + chains = [chains] unless chains.is_a? Enumerable + chains.each do |chain| + case chain + when ::OpenSSL::X509::Certificate + self.add_cert chain + else + next unless File.exists? chain + if File.directory?(chain) + Dir.entries(chain) + .collect { |e| File.join chain, e } + .select { |e| File.file? e } + .each { |f| self.add_file f } + else + self.add_file chain + end + end + end + end + end + end +end + +::OpenSSL::X509::Store.include Fixture::OpenSSL::Store diff --git a/lib/fixtures/integer.rb b/lib/fixtures/integer.rb new file mode 100644 index 0000000..a5656c5 --- /dev/null +++ b/lib/fixtures/integer.rb @@ -0,0 +1,20 @@ +module Fixture + module Integer + def humanize + secs = self + [[60, :second], + [60, :minute], + [24, :hour], + [30, :day], + [12, :month]].map do |count, name| + if secs > 0 + secs, n = secs.divmod count + n = n.to_i + n > 0 ? "#{n} #{name}#{n > 1 ? 's' : ''}" : nil + end + end.compact.reverse.join ' ' + end + end +end + +::Integer.include Fixture::Integer diff --git a/lib/fixtures/string.rb b/lib/fixtures/string.rb new file mode 100644 index 0000000..f1393d5 --- /dev/null +++ b/lib/fixtures/string.rb @@ -0,0 +1,20 @@ +module Fixture + module String + COLORS = { + critical: { color: :white, background: :red }, + error: :red, + warning: :light_red, + good: :green, + great: :blue, + best: :magenta, + unknown: { background: :black } + } + + def colorize(state) + color = COLORS[state] || state + super color + end + end +end + +String.prepend Fixture::String diff --git a/lib/fixtures/tls.rb b/lib/fixtures/tls.rb new file mode 100644 index 0000000..e69de29 diff --git a/set-env b/set-env index cb966e9..1ccdd19 100644 --- a/set-env +++ b/set-env @@ -1,5 +1,20 @@ DIR="$(dirname "$(readlink -e "${BASH_SOURCE:-$0}")")" -export LIBRARY_PATH="$DIR/lib" -export C_INCLUDE_PATH="$DIR/build/openssl/include" -export CPLUS_INCLUDE_PATH="$DIR/build/openssl/include" -export LD_LIBRARY_PATH="$DIR/lib" + +case "$1" in +1.0) + OPENSSL_VERSION=1.0.2j + export RBENV_VERSION=2.3.8-cryptcheck + ;; +1.1) + OPENSSL_VERSION=1.1.1g + export RBENV_VERSION=2.6.6-cryptcheck + ;; +*) + echo "You must provide OpenSSL version to use: 1.0 or 1.1" + exit -1 + ;; +esac + +OPENSSL_PATH="$DIR/build/openssl-$OPENSSL_VERSION" +export LIBRARY_PATH="$OPENSSL_PATH" C_INCLUDE_PATH="$OPENSSL_PATH/include" +export LD_LIBRARY_PATH="$LIBRARY_PATH"