From a465858b49e0d903fe69f652d2f493dc5ea3c44f Mon Sep 17 00:00:00 2001 From: "sofia.dimant" Date: Wed, 21 Jun 2023 12:54:26 +0300 Subject: [PATCH 1/2] Pull slosilo library to Conjur --- .dockerignore | 3 + .gitignore | 20 ++ Gemfile | 2 +- Gemfile.lock | 8 +- gems/slosilo/.kateproject | 4 + gems/slosilo/CHANGELOG.md | 25 ++ gems/slosilo/CONTRIBUTING.md | 16 ++ gems/slosilo/Gemfile | 4 + gems/slosilo/LICENSE | 22 ++ gems/slosilo/README.md | 152 +++++++++++ gems/slosilo/Rakefile | 17 ++ gems/slosilo/SECURITY.md | 42 +++ gems/slosilo/dev/Dockerfile.dev | 7 + gems/slosilo/dev/docker-compose.yml | 8 + gems/slosilo/lib/slosilo.rb | 13 + .../lib/slosilo/adapters/abstract_adapter.rb | 23 ++ .../lib/slosilo/adapters/file_adapter.rb | 42 +++ .../lib/slosilo/adapters/memory_adapter.rb | 31 +++ .../lib/slosilo/adapters/mock_adapter.rb | 21 ++ .../lib/slosilo/adapters/sequel_adapter.rb | 96 +++++++ .../adapters/sequel_adapter/migration.rb | 52 ++++ gems/slosilo/lib/slosilo/attr_encrypted.rb | 85 ++++++ gems/slosilo/lib/slosilo/errors.rb | 15 + gems/slosilo/lib/slosilo/jwt.rb | 122 +++++++++ gems/slosilo/lib/slosilo/key.rb | 218 +++++++++++++++ gems/slosilo/lib/slosilo/keystore.rb | 89 ++++++ gems/slosilo/lib/slosilo/random.rb | 11 + gems/slosilo/lib/slosilo/symmetric.rb | 63 +++++ gems/slosilo/lib/slosilo/version.rb | 3 + gems/slosilo/lib/tasks/slosilo.rake | 32 +++ gems/slosilo/slosilo.gemspec | 34 +++ .../slosilo/spec/encrypted_attributes_spec.rb | 114 ++++++++ gems/slosilo/spec/file_adapter_spec.rb | 81 ++++++ gems/slosilo/spec/jwt_spec.rb | 102 +++++++ gems/slosilo/spec/key_spec.rb | 258 ++++++++++++++++++ gems/slosilo/spec/keystore_spec.rb | 26 ++ gems/slosilo/spec/random_spec.rb | 19 ++ gems/slosilo/spec/sequel_adapter_spec.rb | 171 ++++++++++++ gems/slosilo/spec/slosilo_spec.rb | 124 +++++++++ gems/slosilo/spec/spec_helper.rb | 84 ++++++ gems/slosilo/spec/symmetric_spec.rb | 94 +++++++ gems/slosilo/test.sh | 27 ++ 42 files changed, 2377 insertions(+), 3 deletions(-) create mode 100644 gems/slosilo/.kateproject create mode 100644 gems/slosilo/CHANGELOG.md create mode 100644 gems/slosilo/CONTRIBUTING.md create mode 100644 gems/slosilo/Gemfile create mode 100644 gems/slosilo/LICENSE create mode 100644 gems/slosilo/README.md create mode 100644 gems/slosilo/Rakefile create mode 100644 gems/slosilo/SECURITY.md create mode 100644 gems/slosilo/dev/Dockerfile.dev create mode 100644 gems/slosilo/dev/docker-compose.yml create mode 100644 gems/slosilo/lib/slosilo.rb create mode 100644 gems/slosilo/lib/slosilo/adapters/abstract_adapter.rb create mode 100644 gems/slosilo/lib/slosilo/adapters/file_adapter.rb create mode 100644 gems/slosilo/lib/slosilo/adapters/memory_adapter.rb create mode 100644 gems/slosilo/lib/slosilo/adapters/mock_adapter.rb create mode 100644 gems/slosilo/lib/slosilo/adapters/sequel_adapter.rb create mode 100644 gems/slosilo/lib/slosilo/adapters/sequel_adapter/migration.rb create mode 100644 gems/slosilo/lib/slosilo/attr_encrypted.rb create mode 100644 gems/slosilo/lib/slosilo/errors.rb create mode 100644 gems/slosilo/lib/slosilo/jwt.rb create mode 100644 gems/slosilo/lib/slosilo/key.rb create mode 100644 gems/slosilo/lib/slosilo/keystore.rb create mode 100644 gems/slosilo/lib/slosilo/random.rb create mode 100644 gems/slosilo/lib/slosilo/symmetric.rb create mode 100644 gems/slosilo/lib/slosilo/version.rb create mode 100644 gems/slosilo/lib/tasks/slosilo.rake create mode 100644 gems/slosilo/slosilo.gemspec create mode 100644 gems/slosilo/spec/encrypted_attributes_spec.rb create mode 100644 gems/slosilo/spec/file_adapter_spec.rb create mode 100644 gems/slosilo/spec/jwt_spec.rb create mode 100644 gems/slosilo/spec/key_spec.rb create mode 100644 gems/slosilo/spec/keystore_spec.rb create mode 100644 gems/slosilo/spec/random_spec.rb create mode 100644 gems/slosilo/spec/sequel_adapter_spec.rb create mode 100644 gems/slosilo/spec/slosilo_spec.rb create mode 100644 gems/slosilo/spec/spec_helper.rb create mode 100644 gems/slosilo/spec/symmetric_spec.rb create mode 100755 gems/slosilo/test.sh diff --git a/.dockerignore b/.dockerignore index 0ece2222fb..7244debd3f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -17,11 +17,14 @@ coverage demo dev docker +gems/slosilo/Gemfile.lock +gems/slosilo/spec/reports log package run spec/reports spec/reports-audit + tmp # Ignore directories that are only relevant in gh diff --git a/.gitignore b/.gitignore index ede6e0d417..0708e2dc07 100644 --- a/.gitignore +++ b/.gitignore @@ -73,4 +73,24 @@ conjur_git_commit # AuthnOIDC V2 w/ Identity setup dev/policies/authenticators/authn-oidc/identity-users.yml +gem/slosilo/*.gem +gem/slosilo/*.rbc +gem/slosilo/.bundle +gem/slosilo/.yardoc +gem/slosilo/InstalledFiles +gem/slosilo/_yardoc +gem/slosilo/coverage +gem/slosilo/doc/ +gem/slosilo/lib/bundler/man +gem/slosilo/pkg +gem/slosilo/rdoc +gem/slosilo/spec/reports +gem/slosilo/test/tmp +gem/slosilo/test/version_tmp +gem/slosilo/tmp +gem/slosilo/.rvmrc +gem/slosilo/.project +gem/slosilo/.kateproject.d +gem/slosilo/.idea + VERSION diff --git a/Gemfile b/Gemfile index c736397b45..c4c822a5ff 100644 --- a/Gemfile +++ b/Gemfile @@ -36,7 +36,7 @@ gem 'bcrypt' gem 'gli', require: false gem 'listen' gem 'rexml', '~> 3.2' -gem 'slosilo', '~> 3.0' +gem 'slosilo', path: 'gems/slosilo' # Explicitly required as there are vulnerabilities in older versions gem "ffi", ">= 1.9.24" diff --git a/Gemfile.lock b/Gemfile.lock index b63a5c5945..7e9b330c6f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,6 +13,11 @@ PATH activesupport (>= 4.2) safe_yaml +PATH + remote: gems/slosilo + specs: + slosilo (3.0.1) + GEM remote: https://rubygems.org/ specs: @@ -445,7 +450,6 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) - slosilo (3.0.1) spring (2.1.0) spring-commands-cucumber (1.0.1) spring (>= 0.9.1) @@ -568,7 +572,7 @@ DEPENDENCIES sequel-postgres-schemata sequel-rails simplecov - slosilo (~> 3.0) + slosilo! spring spring-commands-cucumber spring-commands-rspec diff --git a/gems/slosilo/.kateproject b/gems/slosilo/.kateproject new file mode 100644 index 0000000000..4022e4f94e --- /dev/null +++ b/gems/slosilo/.kateproject @@ -0,0 +1,4 @@ +{ + "name": "Slosilo" +, "files": [ { "git": 1 } ] +} diff --git a/gems/slosilo/CHANGELOG.md b/gems/slosilo/CHANGELOG.md new file mode 100644 index 0000000000..d287dc93f5 --- /dev/null +++ b/gems/slosilo/CHANGELOG.md @@ -0,0 +1,25 @@ +# v3.0.1 + + * The symmetric cipher class now encrypts and decrypts in a thread-safe manner. + [cyberark/slosilo#31](https://github.com/cyberark/slosilo/pull/31) + +# v3.0.0 + +* Transition to Ruby 3. Consuming projects based on Ruby 2 shall use slosilo V2.X.X. + +# v2.2.2 + +* Add rake task `slosilo:recalculate_fingerprints` which rehashes the fingerprints in the keystore. +**Note**: After migrating the slosilo keystore, run the above rake task to ensure the fingerprints are correctly hashed. + +# v2.2.1 + +* Use SHA256 algorithm instead of MD5 for public key fingerprints. + +# v2.1.1 + +* Add support for JWT-formatted tokens, with arbitrary expiration. + +# v2.0.1 + +* Fixes a bug that occurs when signing tokens containing Unicode data diff --git a/gems/slosilo/CONTRIBUTING.md b/gems/slosilo/CONTRIBUTING.md new file mode 100644 index 0000000000..7c0a67db14 --- /dev/null +++ b/gems/slosilo/CONTRIBUTING.md @@ -0,0 +1,16 @@ +# Contributing + +For general contribution and community guidelines, please see the [community repo](https://github.com/cyberark/community). + +## Contributing Workflow + +1. [Fork the project](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) +2. [Clone your fork](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository) +3. Make local changes to your fork by editing files +3. [Commit your changes](https://help.github.com/en/github/managing-files-in-a-repository/adding-a-file-to-a-repository-using-the-command-line) +4. [Push your local changes to the remote server](https://help.github.com/en/github/using-git/pushing-commits-to-a-remote-repository) +5. [Create new Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork) + +From here your pull request will be reviewed and once you've responded to all +feedback it will be merged into the project. Congratulations, you're a +contributor! diff --git a/gems/slosilo/Gemfile b/gems/slosilo/Gemfile new file mode 100644 index 0000000000..ace56bf311 --- /dev/null +++ b/gems/slosilo/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in slosilo.gemspec +gemspec diff --git a/gems/slosilo/LICENSE b/gems/slosilo/LICENSE new file mode 100644 index 0000000000..069db73dc1 --- /dev/null +++ b/gems/slosilo/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2020 CyberArk Software Ltd. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/gems/slosilo/README.md b/gems/slosilo/README.md new file mode 100644 index 0000000000..743cf6e7f5 --- /dev/null +++ b/gems/slosilo/README.md @@ -0,0 +1,152 @@ +# Slosilo + +Slosilo is providing a ruby interface to some cryptographic primitives: +- symmetric encryption, +- a mixin for easy encryption of object attributes, +- asymmetric encryption and signing, +- a keystore in a postgres sequel db -- it allows easy storage and retrieval of keys, +- a keystore in files. + +## Installation + +Add this line to your application's Gemfile: + + gem 'slosilo' + +And then execute: + + $ bundle + +## Compatibility + +Version 3.0 introduced full transition to Ruby 3. +Consumers who use slosilo in Ruby 2 projects, shall use slosilo V2.X.X. + +Version 2.0 introduced new symmetric encryption scheme using AES-256-GCM +for authenticated encryption. It allows you to provide AAD on all symmetric +encryption primitives. It's also **NOT COMPATIBLE** with CBC used in version <2. + +This means you'll have to migrate all your existing data. There's no easy way to +do this currently provided; it's recommended to create a database migration and +put relevant code fragments in it directly. (This will also have the benefit of making +the migration self-contained.) + +Since symmetric encryption is used in processing asymetrically encrypted messages, +this incompatibility extends to those too. + +## Usage + +### Symmetric encryption + +```ruby +sym = Slosilo::Symmetric.new +key = sym.random_key +# additional authenticated data +message_id = "message 001" +ciphertext = sym.encrypt "secret message", key: key, aad: message_id +``` + +```ruby +sym = Slosilo::Symmetric.new +message = sym.decrypt ciphertext, key: key, aad: message_id +``` + +### Encryption mixin + +```ruby +require 'slosilo' + +class Foo + attr_accessor :foo + attr_encrypted :foo, aad: :id + + def raw_foo + @foo + end + + def id + "unique record id" + end +end + +Slosilo::encryption_key = Slosilo::Symmetric.new.random_key + +obj = Foo.new +obj.foo = "bar" +obj.raw_foo # => "\xC4\xEF\x87\xD3b\xEA\x12\xDF\xD0\xD4hk\xEDJ\v\x1Cr\xF2#\xA3\x11\xA4*k\xB7\x8F\x8F\xC2\xBD\xBB\xFF\xE3" +obj.foo # => "bar" +``` + +You can safely use it in ie. ActiveRecord::Base or Sequel::Model subclasses. + +### Asymmetric encryption and signing + +```ruby +private_key = Slosilo::Key.new +public_key = private_key.public +``` + +#### Key dumping +```ruby +k = public_key.to_s # => "-----BEGIN PUBLIC KEY----- ... +(Slosilo::Key.new k) == public_key # => true +``` + +#### Encryption + +```ruby +encrypted = public_key.encrypt_message "eagle one sees many clouds" +# => "\xA3\x1A\xD2\xFC\xB0 ... + +public_key.decrypt_message encrypted +# => OpenSSL::PKey::RSAError: private key needed. + +private_key.decrypt_message encrypted +# => "eagle one sees many clouds" +``` + +#### Signing + +```ruby +token = private_key.signed_token "missile launch not authorized" +# => {"data"=>"missile launch not authorized", "timestamp"=>"2014-10-13 12:41:25 UTC", "signature"=>"bSImk...DzV3o", "key"=>"455f7ac42d2d483f750b4c380761821d"} + +public_key.token_valid? token # => true + +token["data"] = "missile launch authorized" +public_key.token_valid? token # => false +``` + +### Keystore + +```ruby +Slosilo::encryption_key = ENV['SLOSILO_KEY'] +Slosilo.adapter = Slosilo::Adapters::FileAdapter.new "~/.keys" + +Slosilo[:own] = Slosilo::Key.new +Slosilo[:their] = Slosilo::Key.new File.read("foo.pem") + +msg = Slosilo[:their].encrypt_message 'bar' +p Slosilo[:own].signed_token msg +``` + +### Keystore in database + +Add a migration to create the necessary table: + + require 'slosilo/adapters/sequel_adapter/migration' + +Remember to migrate your database + + $ rake db:migrate + +Then +```ruby +Slosilo.adapter = Slosilo::Adapters::SequelAdapter.new +``` + +## Contributing + +We welcome contributions of all kinds to this repository. For instructions on +how to get started and descriptions of our development workflows, please see our +[contributing guide](CONTRIBUTING.md). diff --git a/gems/slosilo/Rakefile b/gems/slosilo/Rakefile new file mode 100644 index 0000000000..6130cd547f --- /dev/null +++ b/gems/slosilo/Rakefile @@ -0,0 +1,17 @@ +#!/usr/bin/env rake +require "bundler/gem_tasks" + +begin + require 'rspec/core/rake_task' + RSpec::Core::RakeTask.new(:spec) +rescue LoadError + $stderr.puts "RSpec Rake tasks not available in environment #{ENV['RACK_ENV']}" +end + +task :jenkins do + require 'ci/reporter/rake/rspec' + Rake::Task["ci:setup:rspec"].invoke + Rake::Task["spec"].invoke +end + +task :default => :spec diff --git a/gems/slosilo/SECURITY.md b/gems/slosilo/SECURITY.md new file mode 100644 index 0000000000..5315a3953e --- /dev/null +++ b/gems/slosilo/SECURITY.md @@ -0,0 +1,42 @@ +# Security Policies and Procedures + +This document outlines security procedures and general policies for the CyberArk Conjur +suite of tools and products. + + * [Reporting a Bug](#reporting-a-bug) + * [Disclosure Policy](#disclosure-policy) + * [Comments on this Policy](#comments-on-this-policy) + +## Reporting a Bug + +The CyberArk Conjur team and community take all security bugs in the Conjur suite seriously. +Thank you for improving the security of the Conjur suite. We appreciate your efforts and +responsible disclosure and will make every effort to acknowledge your +contributions. + +Report security bugs by emailing the lead maintainers at security@conjur.org. + +The maintainers will acknowledge your email within 2 business days. Subsequently, we will +send a more detailed response within 2 business days of our acknowledgement indicating +the next steps in handling your report. After the initial reply to your report, the security +team will endeavor to keep you informed of the progress towards a fix and full +announcement, and may ask for additional information or guidance. + +Report security bugs in third-party modules to the person or team maintaining +the module. + +## Disclosure Policy + +When the security team receives a security bug report, they will assign it to a +primary handler. This person will coordinate the fix and release process, +involving the following steps: + + * Confirm the problem and determine the affected versions. + * Audit code to find any potential similar problems. + * Prepare fixes for all releases still under maintenance. These fixes will be + released as fast as possible. + +## Comments on this Policy + +If you have suggestions on how this process could be improved please submit a +pull request. diff --git a/gems/slosilo/dev/Dockerfile.dev b/gems/slosilo/dev/Dockerfile.dev new file mode 100644 index 0000000000..219b658bc6 --- /dev/null +++ b/gems/slosilo/dev/Dockerfile.dev @@ -0,0 +1,7 @@ +FROM ruby + +COPY ./ /src/ + +WORKDIR /src + +RUN bundle diff --git a/gems/slosilo/dev/docker-compose.yml b/gems/slosilo/dev/docker-compose.yml new file mode 100644 index 0000000000..233ec2a628 --- /dev/null +++ b/gems/slosilo/dev/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3' +services: + dev: + build: + context: .. + dockerfile: dev/Dockerfile.dev + volumes: + - ../:/src diff --git a/gems/slosilo/lib/slosilo.rb b/gems/slosilo/lib/slosilo.rb new file mode 100644 index 0000000000..49594f9693 --- /dev/null +++ b/gems/slosilo/lib/slosilo.rb @@ -0,0 +1,13 @@ +require "slosilo/jwt" +require "slosilo/version" +require "slosilo/keystore" +require "slosilo/symmetric" +require "slosilo/attr_encrypted" +require "slosilo/random" +require "slosilo/errors" + +if defined? Sequel + require 'slosilo/adapters/sequel_adapter' + Slosilo::adapter = Slosilo::Adapters::SequelAdapter.new +end +Dir[File.join(File.dirname(__FILE__), 'tasks/*.rake')].each { |ext| load ext } if defined?(Rake) diff --git a/gems/slosilo/lib/slosilo/adapters/abstract_adapter.rb b/gems/slosilo/lib/slosilo/adapters/abstract_adapter.rb new file mode 100644 index 0000000000..5dba4cefaa --- /dev/null +++ b/gems/slosilo/lib/slosilo/adapters/abstract_adapter.rb @@ -0,0 +1,23 @@ +require 'slosilo/attr_encrypted' + +module Slosilo + module Adapters + class AbstractAdapter + def get_key id + raise NotImplementedError + end + + def get_by_fingerprint fp + raise NotImplementedError + end + + def put_key id, key + raise NotImplementedError + end + + def each + raise NotImplementedError + end + end + end +end diff --git a/gems/slosilo/lib/slosilo/adapters/file_adapter.rb b/gems/slosilo/lib/slosilo/adapters/file_adapter.rb new file mode 100644 index 0000000000..428995b4c1 --- /dev/null +++ b/gems/slosilo/lib/slosilo/adapters/file_adapter.rb @@ -0,0 +1,42 @@ +require 'slosilo/adapters/abstract_adapter' + +module Slosilo + module Adapters + class FileAdapter < AbstractAdapter + attr_reader :dir + + def initialize(dir) + @dir = dir + @keys = {} + @fingerprints = {} + Dir[File.join(@dir, "*.key")].each do |f| + key = Slosilo::EncryptedAttributes.decrypt File.read(f) + id = File.basename(f, '.key') + key = @keys[id] = Slosilo::Key.new(key) + @fingerprints[key.fingerprint] = id + end + end + + def put_key id, value + raise "id should not contain a period" if id.index('.') + fname = File.join(dir, "#{id}.key") + File.write(fname, Slosilo::EncryptedAttributes.encrypt(value.to_der)) + File.chmod(0400, fname) + @keys[id] = value + end + + def get_key id + @keys[id] + end + + def get_by_fingerprint fp + id = @fingerprints[fp] + [@keys[id], id] + end + + def each(&block) + @keys.each(&block) + end + end + end +end diff --git a/gems/slosilo/lib/slosilo/adapters/memory_adapter.rb b/gems/slosilo/lib/slosilo/adapters/memory_adapter.rb new file mode 100644 index 0000000000..bfac8eeba3 --- /dev/null +++ b/gems/slosilo/lib/slosilo/adapters/memory_adapter.rb @@ -0,0 +1,31 @@ +require 'slosilo/adapters/abstract_adapter' + +module Slosilo + module Adapters + class MemoryAdapter < AbstractAdapter + def initialize + @keys = {} + @fingerprints = {} + end + + def put_key id, key + key = Slosilo::Key.new(key) if key.is_a?(String) + @keys[id] = key + @fingerprints[key.fingerprint] = id + end + + def get_key id + @keys[id] + end + + def get_by_fingerprint fp + id = @fingerprints[fp] + [@keys[id], id] + end + + def each(&block) + @keys.each(&block) + end + end + end +end diff --git a/gems/slosilo/lib/slosilo/adapters/mock_adapter.rb b/gems/slosilo/lib/slosilo/adapters/mock_adapter.rb new file mode 100644 index 0000000000..d62805fda9 --- /dev/null +++ b/gems/slosilo/lib/slosilo/adapters/mock_adapter.rb @@ -0,0 +1,21 @@ +module Slosilo + module Adapters + class MockAdapter < Hash + def initialize + @fp = {} + end + + def put_key id, key + @fp[key.fingerprint] = id + self[id] = key + end + + alias :get_key :[] + + def get_by_fingerprint fp + id = @fp[fp] + [self[id], id] + end + end + end +end diff --git a/gems/slosilo/lib/slosilo/adapters/sequel_adapter.rb b/gems/slosilo/lib/slosilo/adapters/sequel_adapter.rb new file mode 100644 index 0000000000..b52a75bab6 --- /dev/null +++ b/gems/slosilo/lib/slosilo/adapters/sequel_adapter.rb @@ -0,0 +1,96 @@ +require 'slosilo/adapters/abstract_adapter' + +module Slosilo + module Adapters + class SequelAdapter < AbstractAdapter + def model + @model ||= create_model + end + + def secure? + !Slosilo.encryption_key.nil? + end + + def create_model + model = Sequel::Model(:slosilo_keystore) + model.unrestrict_primary_key + model.attr_encrypted(:key, aad: :id) if secure? + model + end + + def put_key id, value + fail Error::InsecureKeyStorage unless secure? || !value.private? + + attrs = { id: id, key: value.to_der } + attrs[:fingerprint] = value.fingerprint if fingerprint_in_db? + model.create attrs + end + + def get_key id + stored = model[id] + return nil unless stored + Slosilo::Key.new stored.key + end + + def get_by_fingerprint fp + if fingerprint_in_db? + stored = model[fingerprint: fp] + return nil unless stored + [Slosilo::Key.new(stored.key), stored.id] + else + warn "Please migrate to a new database schema using rake slosilo:migrate for efficient fingerprint lookups" + find_by_fingerprint fp + end + end + + def each + model.each do |m| + yield m.id, Slosilo::Key.new(m.key) + end + end + + def recalculate_fingerprints + # Use a transaction to ensure that all fingerprints are updated together. If any update fails, + # we want to rollback all updates. + model.db.transaction do + model.each do |m| + m.update fingerprint: Slosilo::Key.new(m.key).fingerprint + end + end + end + + + def migrate! + unless fingerprint_in_db? + model.db.transaction do + model.db.alter_table :slosilo_keystore do + add_column :fingerprint, String + end + + # reload the schema + model.set_dataset model.dataset + + recalculate_fingerprints + + model.db.alter_table :slosilo_keystore do + set_column_not_null :fingerprint + add_unique_constraint :fingerprint + end + end + end + end + + private + + def fingerprint_in_db? + model.columns.include? :fingerprint + end + + def find_by_fingerprint fp + each do |id, k| + return [k, id] if k.fingerprint == fp + end + end + end + end +end diff --git a/gems/slosilo/lib/slosilo/adapters/sequel_adapter/migration.rb b/gems/slosilo/lib/slosilo/adapters/sequel_adapter/migration.rb new file mode 100644 index 0000000000..7cec637c24 --- /dev/null +++ b/gems/slosilo/lib/slosilo/adapters/sequel_adapter/migration.rb @@ -0,0 +1,52 @@ +require 'sequel' + +module Slosilo + module Adapters::SequelAdapter::Migration + # The default name of the table to hold the keys + DEFAULT_KEYSTORE_TABLE = :slosilo_keystore + + # Sets up default keystore table name + def self.extended(db) + db.keystore_table ||= DEFAULT_KEYSTORE_TABLE + end + + # Keystore table name. If changing this do it immediately after loading the extension. + attr_accessor :keystore_table + + # Create the table for holding keys + def create_keystore_table + # docs say to not use create_table? in migration; + # but we really want this to be robust in case there are any previous installs + # and we can't use table_exists? because it rolls back + create_table? keystore_table do + String :id, primary_key: true + bytea :key, null: false + String :fingerprint, unique: true, null: false + end + end + + # Drop the table + def drop_keystore_table + drop_table keystore_table + end + end + + module Extension + def slosilo_keystore + extend Slosilo::Adapters::SequelAdapter::Migration + end + end + + Sequel::Database.send :include, Extension +end + +Sequel.migration do + up do + slosilo_keystore + create_keystore_table + end + down do + slosilo_keystore + drop_keystore_table + end +end diff --git a/gems/slosilo/lib/slosilo/attr_encrypted.rb b/gems/slosilo/lib/slosilo/attr_encrypted.rb new file mode 100644 index 0000000000..b860dac627 --- /dev/null +++ b/gems/slosilo/lib/slosilo/attr_encrypted.rb @@ -0,0 +1,85 @@ +require 'slosilo/symmetric' + +module Slosilo + # we don't trust the database to keep all backups safe from the prying eyes + # so we encrypt sensitive attributes before storing them + module EncryptedAttributes + module ClassMethods + + # @param options [Hash] + # @option :aad [#to_proc, #to_s] Provide additional authenticated data for + # encryption. This should be something unique to the instance having + # this attribute, such as a primary key; this will ensure that an attacker can't swap + # values around -- trying to decrypt value with a different auth data will fail. + # This means you have to be able to recover it in order to decrypt attributes. + # The following values are accepted: + # + # * Something proc-ish: will be called with self each time auth data is needed. + # * Something stringish: will be to_s-d and used for all instances as auth data. + # Note that this will only prevent swapping in data using another string. + # + # The recommended way to use this option is to pass a proc-ish that identifies the record. + # Note the proc-ish can be a simple method name; for example in case of a Sequel::Model: + # attr_encrypted :secret, aad: :pk + def attr_encrypted *a + options = a.last.is_a?(Hash) ? a.pop : {} + aad = options[:aad] + # note nil.to_s is "", which is exactly the right thing + auth_data = aad.respond_to?(:to_proc) ? aad.to_proc : proc{ |_| aad.to_s } + + # In ruby 3 .arity for #proc returns both 1 and 2, depends on internal #proc + # This method is also being called with aad which is string, in such case the arity is 1 + raise ":aad proc must take two arguments" unless (auth_data.arity.abs == 2 || auth_data.arity.abs == 1) + + # push a module onto the inheritance hierarchy + # this allows calling super in classes + include(accessors = Module.new) + accessors.module_eval do + a.each do |attr| + define_method "#{attr}=" do |value| + super(EncryptedAttributes.encrypt(value, aad: auth_data[self])) + end + define_method attr do + EncryptedAttributes.decrypt(super(), aad: auth_data[self]) + end + end + end + end + + end + + def self.included base + base.extend ClassMethods + end + + class << self + def encrypt value, opts={} + return nil unless value + cipher.encrypt value, key: key, aad: opts[:aad] + end + + def decrypt ctxt, opts={} + return nil unless ctxt + cipher.decrypt ctxt, key: key, aad: opts[:aad] + end + + def key + Slosilo::encryption_key || (raise "Please set Slosilo::encryption_key") + end + + def cipher + @cipher ||= Slosilo::Symmetric.new + end + end + end + + class << self + attr_writer :encryption_key + + def encryption_key + @encryption_key + end + end +end + +Object.send :include, Slosilo::EncryptedAttributes diff --git a/gems/slosilo/lib/slosilo/errors.rb b/gems/slosilo/lib/slosilo/errors.rb new file mode 100644 index 0000000000..abdf35521f --- /dev/null +++ b/gems/slosilo/lib/slosilo/errors.rb @@ -0,0 +1,15 @@ +module Slosilo + class Error < RuntimeError + # An error thrown when attempting to store a private key in an unecrypted + # storage. Set Slosilo.encryption_key to secure the storage or make sure + # to store just the public keys (using Key#public). + class InsecureKeyStorage < Error + def initialize msg = "can't store a private key in a plaintext storage" + super + end + end + + class TokenValidationError < Error + end + end +end diff --git a/gems/slosilo/lib/slosilo/jwt.rb b/gems/slosilo/lib/slosilo/jwt.rb new file mode 100644 index 0000000000..3baf5af790 --- /dev/null +++ b/gems/slosilo/lib/slosilo/jwt.rb @@ -0,0 +1,122 @@ +require 'json' + +module Slosilo + # A JWT-formatted Slosilo token. + # @note This is not intended to be a general-purpose JWT implementation. + class JWT + # Create a new unsigned token with the given claims. + # @param claims [#to_h] claims to embed in this token. + def initialize claims = {} + @claims = JSONHash[claims] + end + + # Parse a token in compact representation + def self.parse_compact raw + load *raw.split('.', 3).map(&Base64.method(:urlsafe_decode64)) + end + + # Parse a token in JSON representation. + # @note only single signature is currently supported. + def self.parse_json raw + raw = JSON.load raw unless raw.respond_to? :to_h + parts = raw.to_h.values_at(*%w(protected payload signature)) + fail ArgumentError, "input not a complete JWT" unless parts.all? + load *parts.map(&Base64.method(:urlsafe_decode64)) + end + + # Add a signature. + # @note currently only a single signature is handled; + # the token will be frozen after this operation. + def add_signature header, &sign + @claims = canonicalize_claims.freeze + @header = JSONHash[header].freeze + @signature = sign[string_to_sign].freeze + freeze + end + + def string_to_sign + [header, claims].map(&method(:encode)).join '.' + end + + # Returns the JSON serialization of this JWT. + def to_json *a + { + protected: encode(header), + payload: encode(claims), + signature: encode(signature) + }.to_json *a + end + + # Returns the compact serialization of this JWT. + def to_s + [header, claims, signature].map(&method(:encode)).join('.') + end + + attr_accessor :claims, :header, :signature + + private + + # Create a JWT token object from existing header, payload, and signature strings. + # @param header [#to_s] URLbase64-encoded representation of the protected header + # @param payload [#to_s] URLbase64-encoded representation of the token payload + # @param signature [#to_s] URLbase64-encoded representation of the signature + def self.load header, payload, signature + self.new(JSONHash.load payload).tap do |token| + token.header = JSONHash.load header + token.signature = signature.to_s.freeze + token.freeze + end + end + + def canonicalize_claims + claims[:iat] = Time.now unless claims.include? :iat + claims[:iat] = claims[:iat].to_time.to_i + claims[:exp] = claims[:exp].to_time.to_i if claims.include? :exp + JSONHash[claims.to_a] + end + + # Convenience method to make the above code clearer. + # Converts to string and urlbase64-encodes. + def encode s + Base64.urlsafe_encode64 s.to_s + end + + # a hash with a possibly frozen JSON stringification + class JSONHash < Hash + def to_s + @repr || to_json + end + + def freeze + @repr = to_json.freeze + super + end + + def self.load raw + self[JSON.load raw.to_s].tap do |h| + h.send :repr=, raw + end + end + + private + + def repr= raw + @repr = raw.freeze + freeze + end + end + end + + # Try to convert by detecting token representation and parsing + def self.JWT raw + if raw.is_a? JWT + raw + elsif raw.respond_to?(:to_h) || raw =~ /\A\s*\{/ + JWT.parse_json raw + else + JWT.parse_compact raw + end + rescue + raise ArgumentError, "invalid value for JWT(): #{raw.inspect}" + end +end diff --git a/gems/slosilo/lib/slosilo/key.rb b/gems/slosilo/lib/slosilo/key.rb new file mode 100644 index 0000000000..54219b0ceb --- /dev/null +++ b/gems/slosilo/lib/slosilo/key.rb @@ -0,0 +1,218 @@ +require 'openssl' +require 'json' +require 'base64' +require 'time' + +require 'slosilo/errors' + +module Slosilo + class Key + def initialize raw_key = nil + @key = if raw_key.is_a? OpenSSL::PKey::RSA + raw_key + elsif !raw_key.nil? + OpenSSL::PKey.read raw_key + else + OpenSSL::PKey::RSA.new 2048 + end + rescue OpenSSL::PKey::PKeyError => e + # old openssl versions used to report ArgumentError + # which arguably makes more sense here, so reraise as that + raise ArgumentError, e, e.backtrace + end + + attr_reader :key + + def cipher + @cipher ||= Slosilo::Symmetric.new + end + + def encrypt plaintext + key = cipher.random_key + ctxt = cipher.encrypt plaintext, key: key + key = @key.public_encrypt key + [ctxt, key] + end + + def encrypt_message plaintext + c, k = encrypt plaintext + k + c + end + + def decrypt ciphertext, skey + key = @key.private_decrypt skey + cipher.decrypt ciphertext, key: key + end + + def decrypt_message ciphertext + k, c = ciphertext.unpack("A256A*") + decrypt c, k + end + + def to_s + @key.public_key.to_pem + end + + def to_der + @to_der ||= @key.to_der + end + + def sign value + sign_string(stringify value) + end + + SIGNATURE_LEN = 256 + + def verify_signature data, signature + signature, salt = signature.unpack("a#{SIGNATURE_LEN}a*") + key.public_decrypt(signature) == hash_function.digest(salt + stringify(data)) + rescue + false + end + + # create a new timestamped and signed token carrying data + def signed_token data + token = { "data" => data, "timestamp" => Time.new.utc.to_s } + token["signature"] = Base64::urlsafe_encode64(sign token) + token["key"] = fingerprint + token + end + + JWT_ALGORITHM = 'conjur.org/slosilo/v2'.freeze + + # Issue a JWT with the given claims. + # `iat` (issued at) claim is automatically added. + # Other interesting claims you can give are: + # - `sub` - token subject, for example a user name; + # - `exp` - expiration time (absolute); + # - `cidr` (Conjur extension) - array of CIDR masks that are accepted to + # make requests that bear this token + def issue_jwt claims + token = Slosilo::JWT.new claims + token.add_signature \ + alg: JWT_ALGORITHM, + kid: fingerprint, + &method(:sign) + token.freeze + end + + DEFAULT_EXPIRATION = 8 * 60 + + def token_valid? token, expiry = DEFAULT_EXPIRATION + return jwt_valid? token if token.respond_to? :header + token = token.clone + expected_key = token.delete "key" + return false if (expected_key and (expected_key != fingerprint)) + signature = Base64::urlsafe_decode64(token.delete "signature") + (Time.parse(token["timestamp"]) + expiry > Time.now) && verify_signature(token, signature) + end + + # Validate a JWT. + # + # Convenience method calling #validate_jwt and returning false if an + # exception is raised. + # + # @param token [JWT] pre-parsed token to verify + # @return [Boolean] + def jwt_valid? token + validate_jwt token + true + rescue + false + end + + # Validate a JWT. + # + # First checks whether algorithm is 'conjur.org/slosilo/v2' and the key id + # matches this key's fingerprint. Then verifies if the token is not expired, + # as indicated by the `exp` claim; in its absence tokens are assumed to + # expire in `iat` + 8 minutes. + # + # If those checks pass, finally the signature is verified. + # + # @raises TokenValidationError if any of the checks fail. + # + # @note It's the responsibility of the caller to examine other claims + # included in the token; consideration needs to be given to handling + # unrecognized claims. + # + # @param token [JWT] pre-parsed token to verify + def validate_jwt token + def err msg + raise Error::TokenValidationError, msg, caller + end + + header = token.header + err 'unrecognized algorithm' unless header['alg'] == JWT_ALGORITHM + err 'mismatched key' if (kid = header['kid']) && kid != fingerprint + iat = Time.at token.claims['iat'] || err('unknown issuing time') + exp = Time.at token.claims['exp'] || (iat + DEFAULT_EXPIRATION) + err 'token expired' if exp <= Time.now + err 'invalid signature' unless verify_signature token.string_to_sign, token.signature + true + end + + def sign_string value + salt = shake_salt + key.private_encrypt(hash_function.digest(salt + value)) + salt + end + + def fingerprint + @fingerprint ||= OpenSSL::Digest::SHA256.hexdigest key.public_key.to_der + end + + def == other + to_der == other.to_der + end + + alias_method :eql?, :== + + def hash + to_der.hash + end + + # return a new key with just the public part of this + def public + Key.new(@key.public_key) + end + + # checks if the keypair contains a private key + def private? + @key.private? + end + + private + + # Note that this is currently somewhat shallow stringification -- + # to implement originating tokens we may need to make it deeper. + def stringify value + string = case value + when Hash + value.to_a.sort.to_json + when String + value + else + value.to_json + end + + # Make sure that the string is ascii_8bit (i.e. raw bytes), and represents + # the utf-8 encoding of the string. This accomplishes two things: it normalizes + # the representation of the string at the byte level (so we don't have an error if + # one username is submitted as ISO-whatever, and the next as UTF-16), and it prevents + # an incompatible encoding error when we concatenate it with the salt. + if string.encoding != Encoding::ASCII_8BIT + string.encode(Encoding::UTF_8).force_encoding(Encoding::ASCII_8BIT) + else + string + end + end + + def shake_salt + Slosilo::Random::salt + end + + def hash_function + @hash_function ||= OpenSSL::Digest::SHA256 + end + end +end diff --git a/gems/slosilo/lib/slosilo/keystore.rb b/gems/slosilo/lib/slosilo/keystore.rb new file mode 100644 index 0000000000..87a13e5970 --- /dev/null +++ b/gems/slosilo/lib/slosilo/keystore.rb @@ -0,0 +1,89 @@ +require 'slosilo/key' + +module Slosilo + class Keystore + def adapter + Slosilo::adapter or raise "No Slosilo adapter is configured or available" + end + + def put id, key + id = id.to_s + fail ArgumentError, "id can't be empty" if id.empty? + adapter.put_key id, key + end + + def get opts + id, fingerprint = opts.is_a?(Hash) ? [nil, opts[:fingerprint]] : [opts, nil] + if id + key = adapter.get_key(id.to_s) + elsif fingerprint + key, _ = get_by_fingerprint(fingerprint) + end + key + end + + def get_by_fingerprint fingerprint + adapter.get_by_fingerprint fingerprint + end + + def each &_ + adapter.each { |k, v| yield k, v } + end + + def any? &block + each do |_, k| + return true if yield k + end + return false + end + end + + class << self + def []= id, value + keystore.put id, value + end + + def [] id + keystore.get id + end + + def each(&block) + keystore.each(&block) + end + + def sign object + self[:own].sign object + end + + def token_valid? token + keystore.any? { |k| k.token_valid? token } + end + + # Looks up the signer by public key fingerprint and checks the validity + # of the signature. If the token is JWT, exp and/or iat claims are also + # verified; the caller is responsible for validating any other claims. + def token_signer token + begin + # see if maybe it's a JWT + token = JWT token + fingerprint = token.header['kid'] + rescue ArgumentError + fingerprint = token['key'] + end + + key, id = keystore.get_by_fingerprint fingerprint + if key && key.token_valid?(token) + return id + else + return nil + end + end + + attr_accessor :adapter + + private + def keystore + @keystore ||= Keystore.new + end + end +end diff --git a/gems/slosilo/lib/slosilo/random.rb b/gems/slosilo/lib/slosilo/random.rb new file mode 100644 index 0000000000..d78ae58578 --- /dev/null +++ b/gems/slosilo/lib/slosilo/random.rb @@ -0,0 +1,11 @@ +require 'openssl' + +module Slosilo + module Random + class << self + def salt + OpenSSL::Random::random_bytes 32 + end + end + end +end diff --git a/gems/slosilo/lib/slosilo/symmetric.rb b/gems/slosilo/lib/slosilo/symmetric.rb new file mode 100644 index 0000000000..7c783f0bc0 --- /dev/null +++ b/gems/slosilo/lib/slosilo/symmetric.rb @@ -0,0 +1,63 @@ +module Slosilo + class Symmetric + VERSION_MAGIC = 'G' + TAG_LENGTH = 16 + + def initialize + @cipher = OpenSSL::Cipher.new 'aes-256-gcm' # NB: has to be lower case for whatever reason. + @cipher_mutex = Mutex.new + end + + # This lets us do a final sanity check in migrations from older encryption versions + def cipher_name + @cipher.name + end + + def encrypt plaintext, opts = {} + # All of these operations in OpenSSL must occur atomically, so we + # synchronize their access to make this step thread-safe. + @cipher_mutex.synchronize do + @cipher.reset + @cipher.encrypt + @cipher.key = (opts[:key] or raise("missing :key option")) + @cipher.iv = iv = random_iv + @cipher.auth_data = opts[:aad] || "" # Nothing good happens if you set this to nil, or don't set it at all + ctext = @cipher.update(plaintext) + @cipher.final + tag = @cipher.auth_tag(TAG_LENGTH) + "#{VERSION_MAGIC}#{tag}#{iv}#{ctext}" + end + end + + def decrypt ciphertext, opts = {} + version, tag, iv, ctext = unpack ciphertext + + raise "Invalid version magic: expected #{VERSION_MAGIC} but was #{version}" unless version == VERSION_MAGIC + + # All of these operations in OpenSSL must occur atomically, so we + # synchronize their access to make this step thread-safe. + @cipher_mutex.synchronize do + @cipher.reset + @cipher.decrypt + @cipher.key = opts[:key] + @cipher.iv = iv + @cipher.auth_tag = tag + @cipher.auth_data = opts[:aad] || "" + @cipher.update(ctext) + @cipher.final + end + end + + def random_iv + @cipher.random_iv + end + + def random_key + @cipher.random_key + end + + private + # return tag, iv, ctext + def unpack msg + msg.unpack "aa#{TAG_LENGTH}a#{@cipher.iv_len}a*" + end + end +end diff --git a/gems/slosilo/lib/slosilo/version.rb b/gems/slosilo/lib/slosilo/version.rb new file mode 100644 index 0000000000..27091ff1a3 --- /dev/null +++ b/gems/slosilo/lib/slosilo/version.rb @@ -0,0 +1,3 @@ +module Slosilo + VERSION = "3.0.1" +end diff --git a/gems/slosilo/lib/tasks/slosilo.rake b/gems/slosilo/lib/tasks/slosilo.rake new file mode 100644 index 0000000000..5cf153faa4 --- /dev/null +++ b/gems/slosilo/lib/tasks/slosilo.rake @@ -0,0 +1,32 @@ +namespace :slosilo do + desc "Dump a public key" + task :dump, [:name] => :environment do |t, args| + args.with_defaults(:name => :own) + puts Slosilo[args[:name]] + end + + desc "Enroll a key" + task :enroll, [:name] => :environment do |t, args| + key = Slosilo::Key.new STDIN.read + Slosilo[args[:name]] = key + puts key + end + + desc "Generate a key pair" + task :generate, [:name] => :environment do |t, args| + args.with_defaults(:name => :own) + key = Slosilo::Key.new + Slosilo[args[:name]] = key + puts key + end + + desc "Migrate to a new database schema" + task :migrate => :environment do |t| + Slosilo.adapter.migrate! + end + + desc "Recalculate fingerprints in keystore" + task :recalculate_fingerprints => :environment do |t| + Slosilo.adapter.recalculate_fingerprints + end +end diff --git a/gems/slosilo/slosilo.gemspec b/gems/slosilo/slosilo.gemspec new file mode 100644 index 0000000000..f0df057074 --- /dev/null +++ b/gems/slosilo/slosilo.gemspec @@ -0,0 +1,34 @@ +# -*- encoding: utf-8 -*- +begin + require File.expand_path('../lib/slosilo/version', __FILE__) +rescue LoadError + # so that bundle can be run without the app code + module Slosilo + VERSION = '0.0.0' + end +end + +Gem::Specification.new do |gem| + gem.name = "slosilo" + gem.version = Slosilo::VERSION + gem.authors = ["Cyberark R&D"] + gem.summary = %q{Store SSL keys in a database} + gem.description = %q{This gem provides an easy way of storing and retrieving encryption keys in the database.} + gem.homepage = "" + + gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } + gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) + + gem.require_paths = ["lib"] + gem.required_ruby_version = '>= 3.0.0' + + gem.add_development_dependency 'rake' + gem.add_development_dependency 'rspec', '~> 3.0' + gem.add_development_dependency 'ci_reporter_rspec' + gem.add_development_dependency 'simplecov' + gem.add_development_dependency 'simplecov-cobertura' + gem.add_development_dependency 'io-grab', '~> 0.0.1' + gem.add_development_dependency 'sequel' # for sequel tests + gem.add_development_dependency 'sqlite3' # for sequel tests + gem.add_development_dependency 'activesupport' # for convenience in specs +end diff --git a/gems/slosilo/spec/encrypted_attributes_spec.rb b/gems/slosilo/spec/encrypted_attributes_spec.rb new file mode 100644 index 0000000000..d2828d9ce9 --- /dev/null +++ b/gems/slosilo/spec/encrypted_attributes_spec.rb @@ -0,0 +1,114 @@ +require 'spec_helper' +require 'slosilo/attr_encrypted' + +describe Slosilo::EncryptedAttributes do + before(:all) do + Slosilo::encryption_key = OpenSSL::Cipher.new("aes-256-gcm").random_key + end + + let(:aad) { proc{ |_| "hithere" } } + + let(:base){ + Class.new do + attr_accessor :normal_ivar,:with_aad + def stupid_ivar + side_effect! + @_explicit + end + def stupid_ivar= e + side_effect! + @_explicit = e + end + def side_effect! + + end + end + } + + let(:sub){ + Class.new(base) do + attr_encrypted :normal_ivar, :stupid_ivar + end + } + + subject{ sub.new } + + context "when setting a normal ivar" do + let(:value){ "some value" } + it "stores an encrypted value in the ivar" do + subject.normal_ivar = value + expect(subject.instance_variable_get(:"@normal_ivar")).to_not eq(value) + end + + it "recovers the value set" do + subject.normal_ivar = value + expect(subject.normal_ivar).to eq(value) + end + end + + context "when setting an attribute with an implementation" do + it "calls the base class method" do + expect(subject).to receive_messages(:side_effect! => nil) + subject.stupid_ivar = "hi" + expect(subject.stupid_ivar).to eq("hi") + end + end + + context "when given an :aad option" do + + let(:cipher){ Slosilo::EncryptedAttributes.cipher } + let(:key){ Slosilo::EncryptedAttributes.key} + context "that is a string" do + let(:aad){ "hello there" } + before{ sub.attr_encrypted :with_aad, aad: aad } + it "encrypts the value with the given string for auth data" do + expect(cipher).to receive(:encrypt).with("hello", key: key, aad: aad) + subject.with_aad = "hello" + end + + it "decrypts the encrypted value" do + subject.with_aad = "foo" + expect(subject.with_aad).to eq("foo") + end + end + + context "that is nil" do + let(:aad){ nil } + before{ sub.attr_encrypted :with_aad, aad: aad } + it "encrypts the value with an empty string for auth data" do + expect(cipher).to receive(:encrypt).with("hello",key: key, aad: "").and_call_original + subject.with_aad = "hello" + end + + it "decrypts the encrypted value" do + subject.with_aad = "hello" + expect(subject.with_aad).to eq("hello") + end + end + + context "that is a proc" do + let(:aad){ + proc{ |o| "x" } + } + + before{ sub.attr_encrypted :with_aad, aad: aad } + + it "calls the proc with the object being encrypted" do + expect(aad).to receive(:[]).with(subject).and_call_original + subject.with_aad = "hi" + end + + it "encrypts the value with the string returned for auth data" do + expect(cipher).to receive(:encrypt).with("hello", key: key, aad: aad[subject]).and_call_original + subject.with_aad = "hello" + end + it "decrypts the encrypted value" do + subject.with_aad = "hello" + expect(subject.with_aad).to eq("hello") + end + end + + end + + +end diff --git a/gems/slosilo/spec/file_adapter_spec.rb b/gems/slosilo/spec/file_adapter_spec.rb new file mode 100644 index 0000000000..6919efc6b5 --- /dev/null +++ b/gems/slosilo/spec/file_adapter_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' +require 'tmpdir' + +require 'slosilo/adapters/file_adapter' + +describe Slosilo::Adapters::FileAdapter do + include_context "with example key" + + let(:dir) { Dir.mktmpdir } + let(:adapter) { Slosilo::Adapters::FileAdapter.new dir } + subject { adapter } + + describe "#get_key" do + context "when given key does not exist" do + it "returns nil" do + expect(subject.get_key(:whatever)).not_to be + end + end + end + + describe "#put_key" do + context "unacceptable id" do + let(:id) { "foo.bar" } + it "isn't accepted" do + expect { subject.put_key id, key }.to raise_error /id should not contain a period/ + end + end + context "acceptable id" do + let(:id) { "id" } + let(:key_encrypted) { "encrypted key" } + let(:fname) { "#{dir}/#{id}.key" } + it "creates the key" do + expect(Slosilo::EncryptedAttributes).to receive(:encrypt).with(key.to_der).and_return key_encrypted + expect(File).to receive(:write).with(fname, key_encrypted) + expect(File).to receive(:chmod).with(0400, fname) + subject.put_key id, key + expect(subject.instance_variable_get("@keys")[id]).to eq(key) + end + end + end + + describe "#each" do + before { adapter.instance_variable_set("@keys", one: :onek, two: :twok) } + + it "iterates over each key" do + results = [] + adapter.each { |id,k| results << { id => k } } + expect(results).to eq([ { one: :onek}, {two: :twok } ]) + end + end + + context 'with real key store' do + let(:id) { 'some id' } + + before do + Slosilo::encryption_key = Slosilo::Symmetric.new.random_key + pre_adapter = Slosilo::Adapters::FileAdapter.new dir + pre_adapter.put_key(id, key) + end + + describe '#get_key' do + it "loads and decrypts the key" do + expect(adapter.get_key(id)).to eq(key) + end + end + + describe '#get_by_fingerprint' do + it "can look up a key by a fingerprint" do + expect(adapter.get_by_fingerprint(key_fingerprint)).to eq([key, id]) + end + end + + describe '#each' do + it "enumerates the keys" do + results = [] + adapter.each { |id,k| results << { id => k } } + expect(results).to eq([ { id => key } ]) + end + end + end +end diff --git a/gems/slosilo/spec/jwt_spec.rb b/gems/slosilo/spec/jwt_spec.rb new file mode 100644 index 0000000000..eed3bfb233 --- /dev/null +++ b/gems/slosilo/spec/jwt_spec.rb @@ -0,0 +1,102 @@ +require 'spec_helper' + +# (Mostly) integration tests for JWT token format +describe Slosilo::Key do + include_context "with example key" + + describe '#issue_jwt' do + it 'issues an JWT token with given claims' do + allow(Time).to receive(:now) { DateTime.parse('2014-06-04 23:22:32 -0400').to_time } + + tok = key.issue_jwt sub: 'host/example', cidr: %w(fec0::/64) + + expect(tok).to be_frozen + + expect(tok.header).to eq \ + alg: 'conjur.org/slosilo/v2', + kid: key_fingerprint + expect(tok.claims).to eq \ + iat: 1401938552, + sub: 'host/example', + cidr: ['fec0::/64'] + + expect(key.verify_signature tok.string_to_sign, tok.signature).to be_truthy + end + end +end + +describe Slosilo::JWT do + context "with a signed token" do + let(:signature) { 'very signed, such alg' } + subject(:token) { Slosilo::JWT.new test: "token" } + before do + allow(Time).to receive(:now) { DateTime.parse('2014-06-04 23:22:32 -0400').to_time } + token.add_signature(alg: 'test-sig') { signature } + end + + it 'allows conversion to JSON representation with #to_json' do + json = JSON.load token.to_json + expect(JSON.load Base64.urlsafe_decode64 json['protected']).to eq \ + 'alg' => 'test-sig' + expect(JSON.load Base64.urlsafe_decode64 json['payload']).to eq \ + 'iat' => 1401938552, 'test' => 'token' + expect(Base64.urlsafe_decode64 json['signature']).to eq signature + end + + it 'allows conversion to compact representation with #to_s' do + h, c, s = token.to_s.split '.' + expect(JSON.load Base64.urlsafe_decode64 h).to eq \ + 'alg' => 'test-sig' + expect(JSON.load Base64.urlsafe_decode64 c).to eq \ + 'iat' => 1401938552, 'test' => 'token' + expect(Base64.urlsafe_decode64 s).to eq signature + end + end + + describe '#to_json' do + it "passes any parameters" do + token = Slosilo::JWT.new + allow(token).to receive_messages \ + header: :header, + claims: :claims, + signature: :signature + expect_any_instance_of(Hash).to receive(:to_json).with :testing + expect(token.to_json :testing) + end + end + + describe '()' do + include_context "with example key" + + it 'understands both serializations' do + [COMPACT_TOKEN, JSON_TOKEN].each do |token| + token = Slosilo::JWT token + expect(token.header).to eq \ + 'typ' => 'JWT', + 'alg' => 'conjur.org/slosilo/v2', + 'kid' => key_fingerprint + expect(token.claims).to eq \ + 'sub' => 'host/example', + 'iat' => 1401938552, + 'exp' => 1401938552 + 60*60, + 'cidr' => ['fec0::/64'] + expect(key.verify_signature token.string_to_sign, token.signature).to be_truthy + end + end + + it 'is a noop if already parsed' do + token = Slosilo::JWT COMPACT_TOKEN + expect(Slosilo::JWT token).to eq token + end + + it 'raises ArgumentError on failure to convert' do + expect { Slosilo::JWT "foo bar" }.to raise_error ArgumentError + expect { Slosilo::JWT elite: 31337 }.to raise_error ArgumentError + expect { Slosilo::JWT "foo.bar.xyzzy" }.to raise_error ArgumentError + end + end + + COMPACT_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25qdXIub3JnL3Nsb3NpbG8vdjIiLCJraWQiOiIxMDdiZGI4NTAxYzQxOWZhZDJmZGIyMGI0NjdkNGQwYTYyYTE2YTk4YzM1ZjJkYTBlYjNiMWZmOTI5Nzk1YWQ5In0=.eyJzdWIiOiJob3N0L2V4YW1wbGUiLCJjaWRyIjpbImZlYzA6Oi82NCJdLCJleHAiOjE0MDE5NDIxNTIsImlhdCI6MTQwMTkzODU1Mn0=.qSxy6gx0DbiIc-Wz_vZhBsYi1SCkHhzxfMGPnnG6MTqjlzy7ntmlU2H92GKGoqCRo6AaNLA_C3hA42PeEarV5nMoTj8XJO_kwhrt2Db2OX4u83VS0_enoztWEZG5s45V0Lv71lVR530j4LD-hpqhm_f4VuISkeH84u0zX7s1zKOlniuZP-abCAHh0htTnrVz9wKG0VywkCUmWYyNNqC2h8PRf64SvCWcQ6VleHpjO-ms8OeTw4ZzRbzKMi0mL6eTmQlbT3PeBArUaS0pNJPg9zdDQaL2XDOofvQmj6Yy_8RA4eCt9HEfTYEdriVqK-_9QCspbGzFVn9GTWf51MRi5dngV9ItsDoG9ktDtqFuMttv7TcqjftsIHZXZsAZ175E".freeze + + JSON_TOKEN = "{\"protected\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25qdXIub3JnL3Nsb3NpbG8vdjIiLCJraWQiOiIxMDdiZGI4NTAxYzQxOWZhZDJmZGIyMGI0NjdkNGQwYTYyYTE2YTk4YzM1ZjJkYTBlYjNiMWZmOTI5Nzk1YWQ5In0=\",\"payload\":\"eyJzdWIiOiJob3N0L2V4YW1wbGUiLCJjaWRyIjpbImZlYzA6Oi82NCJdLCJleHAiOjE0MDE5NDIxNTIsImlhdCI6MTQwMTkzODU1Mn0=\",\"signature\":\"qSxy6gx0DbiIc-Wz_vZhBsYi1SCkHhzxfMGPnnG6MTqjlzy7ntmlU2H92GKGoqCRo6AaNLA_C3hA42PeEarV5nMoTj8XJO_kwhrt2Db2OX4u83VS0_enoztWEZG5s45V0Lv71lVR530j4LD-hpqhm_f4VuISkeH84u0zX7s1zKOlniuZP-abCAHh0htTnrVz9wKG0VywkCUmWYyNNqC2h8PRf64SvCWcQ6VleHpjO-ms8OeTw4ZzRbzKMi0mL6eTmQlbT3PeBArUaS0pNJPg9zdDQaL2XDOofvQmj6Yy_8RA4eCt9HEfTYEdriVqK-_9QCspbGzFVn9GTWf51MRi5dngV9ItsDoG9ktDtqFuMttv7TcqjftsIHZXZsAZ175E\"}".freeze +end diff --git a/gems/slosilo/spec/key_spec.rb b/gems/slosilo/spec/key_spec.rb new file mode 100644 index 0000000000..5ed444fa60 --- /dev/null +++ b/gems/slosilo/spec/key_spec.rb @@ -0,0 +1,258 @@ +require 'spec_helper' + +require 'active_support' +require 'active_support/core_ext/numeric/time' + +describe Slosilo::Key do + include_context "with example key" + + subject { key } + + describe '#to_der' do + subject { super().to_der } + it { is_expected.to eq(rsa.to_der) } + end + + describe '#to_s' do + subject { super().to_s } + it { is_expected.to eq(rsa.public_key.to_pem) } + end + + describe '#fingerprint' do + subject { super().fingerprint } + it { is_expected.to eq(key_fingerprint) } + end + it { is_expected.to be_private } + + context "with identical key" do + let(:other) { Slosilo::Key.new rsa.to_der } + it "is equal" do + expect(subject).to eq(other) + end + + it "is eql?" do + expect(subject.eql?(other)).to be_truthy + end + + it "has equal hash" do + expect(subject.hash).to eq(other.hash) + end + end + + context "with a different key" do + let(:other) { Slosilo::Key.new another_rsa } + it "is not equal" do + expect(subject).not_to eq(other) + end + + it "is not eql?" do + expect(subject.eql?(other)).not_to be_truthy + end + + it "has different hash" do + expect(subject.hash).not_to eq(other.hash) + end + end + + describe '#public' do + it "returns a key with just the public half" do + pkey = subject.public + expect(pkey).to be_a(Slosilo::Key) + expect(pkey).to_not be_private + expect(pkey.key).to_not be_private + expect(pkey.to_der).to eq(rsa.public_key.to_der) + end + end + + let(:plaintext) { 'quick brown fox jumped over the lazy dog' } + describe '#encrypt' do + it "generates a symmetric encryption key and encrypts the plaintext with the public key" do + ctxt, skey = subject.encrypt plaintext + pskey = rsa.private_decrypt skey + expect(Slosilo::Symmetric.new.decrypt(ctxt, key: pskey)).to eq(plaintext) + end + end + + describe '#encrypt_message' do + it "#encrypts a message and then returns the result as a single string" do + expect(subject).to receive(:encrypt).with(plaintext).and_return ['fake ciphertext', 'fake key'] + expect(subject.encrypt_message(plaintext)).to eq('fake keyfake ciphertext') + end + end + + let(:ciphertext){ "G\xAD^\x17\x11\xBBQ9-b\x14\xF6\x92#Q0x\xF4\xAD\x1A\x92\xC3VZW\x89\x8E\x8Fg\x93\x05B\xF8\xD6O\xCFGCTp\b~\x916\xA3\x9AN\x8D\x961\x1F\xA3mSf&\xAD\xA77/]z\xA89\x01\xA7\xA9\x92\f".force_encoding('ASCII-8BIT') } + let(:skey){ "\x82\x93\xFAA\xA6wQA\xE1\xB5\xA6b\x8C.\xCF#I\x86I\x83u\x99\rTA\xEF\xC4\x91\xC5)-\xEBQ\xB1\xC0\xC6\xFF\x90L\xFE\x1E\x15\x81\x12\x16\xDD:A\xC5d\xE1B\xD2f@\xB8o\xB7+N\xB7\n\x92\xDC\x9E\xE3\x83\xB8>h\a\xC7\xCC\xCF\xD0t\x06\x8B\xA8\xBF\xEFe\xA4{\x88\f\xDD\roF\xEB.\xDA\xBF\x9D_0>\xF03c'\x1F!)*-\x19\x97\xAC\xD2\x1F(,6h\a\x93\xDB\x8E\x97\xF9\x1A\x11\x84\x11t\xD9\xB2\x85\xB0\x12\x7F\x03\x00O\x8F\xBE#\xFFb\xA5w\xF3g\xCF\xB4\xF2\xB7\xDBiA=\xA8\xFD1\xEC\xBF\xD7\x8E\xB6W>\x03\xACNBa\xBF\xFD\xC6\xB32\x8C\xE2\xF1\x87\x9C\xAE6\xD1\x12\vkl\xBB\xA0\xED\x9A\xEE6\xF2\xD9\xB4LL\xE2h/u_\xA1i=\x11x\x8DGha\x8EG\b+\x84[\x87\x8E\x01\x0E\xA5\xB0\x9F\xE9vSl\x18\xF3\xEA\xF4NH\xA8\xF1\x81\xBB\x98\x01\xE8p]\x18\x11f\xA3K\xA87c\xBB\x13X~K\xA2".force_encoding('ASCII-8BIT') } + describe '#decrypt' do + it "decrypts the symmetric key and then uses it to decrypt the ciphertext" do + expect(subject.decrypt(ciphertext, skey)).to eq(plaintext) + end + end + + describe '#decrypt_message' do + it "splits the message into key and rest, then #decrypts it" do + expect(subject).to receive(:decrypt).with(ciphertext, skey).and_return plaintext + expect(subject.decrypt_message(skey + ciphertext)).to eq(plaintext) + end + end + + describe '#initialize' do + context "when no argument given" do + subject { Slosilo::Key.new } + let (:rsa) { double "key" } + it "generates a new key pair" do + expect(OpenSSL::PKey::RSA).to receive(:new).with(2048).and_return(rsa) + expect(subject.key).to eq(rsa) + end + end + context "when given an armored key" do + subject { Slosilo::Key.new rsa.to_der } + + describe '#to_der' do + subject { super().to_der } + it { is_expected.to eq(rsa.to_der) } + end + end + context "when given a key instance" do + subject { Slosilo::Key.new rsa } + + describe '#to_der' do + subject { super().to_der } + it { is_expected.to eq(rsa.to_der) } + end + end + context "when given something else" do + subject { Slosilo::Key.new "foo" } + it "fails early" do + expect { subject }.to raise_error ArgumentError + end + end + end + + describe "#sign" do + context "when given a hash" do + it "converts to a sorted array and signs that" do + expect(key).to receive(:sign_string).with '[["a",3],["b",42]]' + key.sign b: 42, a: 3 + end + end + context "when given an array" do + it "signs a JSON representation instead" do + expect(key).to receive(:sign_string).with '[2,[42,2]]' + key.sign [2, [42, 2]] + end + end + context "when given a string" do + let(:expected_signature) { "d[\xA4\x00\x02\xC5\x17\xF5P\x1AD\x91\xF9\xC1\x00P\x0EG\x14,IN\xDE\x17\xE1\xA2a\xCC\xABR\x99'\xB0A\xF5~\x93M/\x95-B\xB1\xB6\x92!\x1E\xEA\x9C\v\xC2O\xA8\x91\x1C\xF9\x11\x92a\xBFxm-\x93\x9C\xBBoM\x92%\xA9\xD06$\xC1\xBC.`\xF8\x03J\x16\xE1\xB0c\xDD\xBF\xB0\xAA\xD7\xD4\xF4\xFC\e*\xAB\x13A%-\xD3\t\xA5R\x18\x01let6\xC8\xE9\"\x7F6O\xC7p\x82\xAB\x04J(IY\xAA]b\xA4'\xD6\x873`\xAB\x13\x95g\x9C\x17\xCAB\xF8\xB9\x85B:^\xC5XY^\x03\xEA\xB6V\x17b2\xCA\xF5\xD6\xD4\xD2\xE3u\x11\xECQ\x0Fb\x14\xE2\x04\xE1 unicode} } + + it "converts the value to raw bytes before signing it" do + expect(key).to receive(:sign_string).with("[[\"data\",\"#{encoded}\"]]").and_call_original + key.sign hash + end + end + end + + describe "#signed_token" do + let(:time) { Time.new(2012,1,1,1,1,1,0) } + let(:data) { { "foo" => :bar } } + let(:token_to_sign) { { "data" => data, "timestamp" => "2012-01-01 01:01:01 UTC" } } + let(:signature) { "signature" } + let(:salt) { 'a pinch of salt' } + let(:expected_signature) { Base64::urlsafe_encode64 "\xB0\xCE{\x9FP\xEDV\x9C\xE7b\x8B[\xFAil\x87^\x96\x17Z\x97\x1D\xC2?B\x96\x9C\x8Ep-\xDF_\x8F\xC21\xD9^\xBC\n\x16\x04\x8DJ\xF6\xAF-\xEC\xAD\x03\xF9\xEE:\xDF\xB5\x8F\xF9\xF6\x81m\xAB\x9C\xAB1\x1E\x837\x8C\xFB\xA8P\xA8<\xEA\x1Dx\xCEd\xED\x84f\xA7\xB5t`\x96\xCC\x0F\xA9t\x8B\x9Fo\xBF\x92K\xFA\xFD\xC5?\x8F\xC68t\xBC\x9F\xDE\n$\xCA\xD2\x8F\x96\x0EtX2\x8Cl\x1E\x8Aa\r\x8D\xCAi\x86\x1A\xBD\x1D\xF7\xBC\x8561j\x91YlO\xFA(\x98\x10iq\xCC\xAF\x9BV\xC6\v\xBC\x10Xm\xCD\xFE\xAD=\xAA\x95,\xB4\xF7\xE8W\xB8\x83;\x81\x88\xE6\x01\xBA\xA5F\x91\x17\f\xCE\x80\x8E\v\x83\x9D<\x0E\x83\xF6\x8D\x03\xC0\xE8A\xD7\x90i\x1D\x030VA\x906D\x10\xA0\xDE\x12\xEF\x06M\xD8\x8B\xA9W\xC8\x9DTc\x8AJ\xA4\xC0\xD3!\xFA\x14\x89\xD1p\xB4J7\xA5\x04\xC2l\xDC8<\x04Y\xD8\xA4\xFB[\x89\xB1\xEC\xDA\xB8\xD7\xEA\x03Ja pinch of salt".force_encoding("ASCII-8BIT") } + let(:expected_token) { token_to_sign.merge "signature" => expected_signature, "key" => key_fingerprint } + before do + allow(key).to receive_messages shake_salt: salt + allow(Time).to receive_messages new: time + end + subject { key.signed_token data } + it { is_expected.to eq(expected_token) } + end + + describe "#validate_jwt" do + let(:token) do + instance_double Slosilo::JWT, + header: { 'alg' => 'conjur.org/slosilo/v2' }, + claims: { 'iat' => Time.now.to_i }, + string_to_sign: double("string to sign"), + signature: double("signature") + end + + before do + allow(key).to receive(:verify_signature).with(token.string_to_sign, token.signature) { true } + end + + it "verifies the signature" do + expect { key.validate_jwt token }.not_to raise_error + end + + it "rejects unknown algorithm" do + token.header['alg'] = 'HS256' # we're not supporting standard algorithms + expect { key.validate_jwt token }.to raise_error /algorithm/ + end + + it "rejects bad signature" do + allow(key).to receive(:verify_signature).with(token.string_to_sign, token.signature) { false } + expect { key.validate_jwt token }.to raise_error /signature/ + end + + it "rejects expired token" do + token.claims['exp'] = 1.hour.ago.to_i + expect { key.validate_jwt token }.to raise_error /expired/ + end + + it "accepts unexpired token with implicit expiration" do + token.claims['iat'] = 5.minutes.ago + expect { key.validate_jwt token }.to_not raise_error + end + + it "rejects token expired with implicit expiration" do + token.claims['iat'] = 10.minutes.ago.to_i + expect { key.validate_jwt token }.to raise_error /expired/ + end + end + + describe "#token_valid?" do + let(:data) { { "foo" => :bar } } + let(:signature) { Base64::urlsafe_encode64 "\xB0\xCE{\x9FP\xEDV\x9C\xE7b\x8B[\xFAil\x87^\x96\x17Z\x97\x1D\xC2?B\x96\x9C\x8Ep-\xDF_\x8F\xC21\xD9^\xBC\n\x16\x04\x8DJ\xF6\xAF-\xEC\xAD\x03\xF9\xEE:\xDF\xB5\x8F\xF9\xF6\x81m\xAB\x9C\xAB1\x1E\x837\x8C\xFB\xA8P\xA8<\xEA\x1Dx\xCEd\xED\x84f\xA7\xB5t`\x96\xCC\x0F\xA9t\x8B\x9Fo\xBF\x92K\xFA\xFD\xC5?\x8F\xC68t\xBC\x9F\xDE\n$\xCA\xD2\x8F\x96\x0EtX2\x8Cl\x1E\x8Aa\r\x8D\xCAi\x86\x1A\xBD\x1D\xF7\xBC\x8561j\x91YlO\xFA(\x98\x10iq\xCC\xAF\x9BV\xC6\v\xBC\x10Xm\xCD\xFE\xAD=\xAA\x95,\xB4\xF7\xE8W\xB8\x83;\x81\x88\xE6\x01\xBA\xA5F\x91\x17\f\xCE\x80\x8E\v\x83\x9D<\x0E\x83\xF6\x8D\x03\xC0\xE8A\xD7\x90i\x1D\x030VA\x906D\x10\xA0\xDE\x12\xEF\x06M\xD8\x8B\xA9W\xC8\x9DTc\x8AJ\xA4\xC0\xD3!\xFA\x14\x89\xD1p\xB4J7\xA5\x04\xC2l\xDC8<\x04Y\xD8\xA4\xFB[\x89\xB1\xEC\xDA\xB8\xD7\xEA\x03Ja pinch of salt".force_encoding("ASCII-8BIT") } + let(:token) { { "data" => data, "timestamp" => "2012-01-01 01:01:01 UTC", "signature" => signature } } + before { allow(Time).to receive_messages now: Time.new(2012,1,1,1,2,1,0) } + subject { key.token_valid? token } + it { is_expected.to be_truthy } + + it "doesn't check signature on the advisory key field" do + expect(key.token_valid?(token.merge "key" => key_fingerprint)).to be_truthy + end + + it "rejects the token if the key field is present and doesn't match" do + expect(key.token_valid?(token.merge "key" => "this is not the key you are looking for")).not_to be_truthy + end + + context "when token is 1 hour old" do + before { allow(Time).to receive_messages now: Time.new(2012,1,1,2,1,1,0) } + it { is_expected.to be_falsey } + context "when timestamp in the token is changed accordingly" do + let(:token) { { "data" => data, "timestamp" => "2012-01-01 02:00:01 UTC", "signature" => signature } } + it { is_expected.to be_falsey } + end + end + context "when the data is changed" do + let(:data) { { "foo" => :baz } } + it { is_expected.to be_falsey } + end + context "when RSA decrypt raises an error" do + before { expect_any_instance_of(OpenSSL::PKey::RSA).to receive(:public_decrypt).and_raise(OpenSSL::PKey::RSAError) } + it { is_expected.to be_falsey } + end + end +end diff --git a/gems/slosilo/spec/keystore_spec.rb b/gems/slosilo/spec/keystore_spec.rb new file mode 100644 index 0000000000..d11db76b8f --- /dev/null +++ b/gems/slosilo/spec/keystore_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Slosilo::Keystore do + include_context "with example key" + include_context "with mock adapter" + + describe '#put' do + it "handles Slosilo::Keys" do + subject.put(:test, key) + expect(adapter['test'].to_der).to eq(rsa.to_der) + end + + it "refuses to store a key with a nil id" do + expect { subject.put(nil, key) }.to raise_error(ArgumentError) + end + + it "refuses to store a key with an empty id" do + expect { subject.put('', key) }.to raise_error(ArgumentError) + end + + it "passes the Slosilo key to the adapter" do + expect(adapter).to receive(:put_key).with "test", key + subject.put :test, key + end + end +end diff --git a/gems/slosilo/spec/random_spec.rb b/gems/slosilo/spec/random_spec.rb new file mode 100644 index 0000000000..12c3184775 --- /dev/null +++ b/gems/slosilo/spec/random_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe Slosilo::Random do + subject { Slosilo::Random } + let(:other_salt) { Slosilo::Random::salt } + + describe '#salt' do + subject { super().salt } + describe '#length' do + subject { super().length } + it { is_expected.to eq(32) } + end + end + + describe '#salt' do + subject { super().salt } + it { is_expected.not_to eq(other_salt) } + end +end diff --git a/gems/slosilo/spec/sequel_adapter_spec.rb b/gems/slosilo/spec/sequel_adapter_spec.rb new file mode 100644 index 0000000000..af08fbbc2e --- /dev/null +++ b/gems/slosilo/spec/sequel_adapter_spec.rb @@ -0,0 +1,171 @@ +require 'spec_helper' +require 'sequel' +require 'io/grab' + +require 'slosilo/adapters/sequel_adapter' + +describe Slosilo::Adapters::SequelAdapter do + include_context "with example key" + + let(:model) { double "model" } + before { allow(subject).to receive_messages create_model: model } + + describe "#get_key" do + context "when given key does not exist" do + before { allow(model).to receive_messages :[] => nil } + it "returns nil" do + expect(subject.get_key(:whatever)).not_to be + end + end + + context "when it exists" do + let(:id) { "id" } + before { allow(model).to receive(:[]).with(id).and_return (double "key entry", id: id, key: rsa.to_der) } + it "returns it" do + expect(subject.get_key(id)).to eq(key) + end + end + end + + describe "#put_key" do + let(:id) { "id" } + it "creates the key" do + expect(model).to receive(:create).with(hash_including(:id => id, :key => key.to_der)) + allow(model).to receive_messages columns: [:id, :key] + subject.put_key id, key + end + + it "adds the fingerprint if feasible" do + expect(model).to receive(:create).with(hash_including(:id => id, :key => key.to_der, :fingerprint => key.fingerprint)) + allow(model).to receive_messages columns: [:id, :key, :fingerprint] + subject.put_key id, key + end + end + + let(:adapter) { subject } + describe "#each" do + let(:one) { double("one", id: :one, key: :onek) } + let(:two) { double("two", id: :two, key: :twok) } + before { allow(model).to receive(:each).and_yield(one).and_yield(two) } + + it "iterates over each key" do + results = [] + allow(Slosilo::Key).to receive(:new) {|x|x} + adapter.each { |id,k| results << { id => k } } + expect(results).to eq([ { one: :onek}, {two: :twok } ]) + end + end + + shared_context "database" do + let(:db) { Sequel.sqlite } + before do + allow(subject).to receive(:create_model).and_call_original + Sequel::Model.cache_anonymous_models = false + Sequel::Model.db = db + end + end + + shared_context "encryption key" do + before do + Slosilo.encryption_key = Slosilo::Symmetric.new.random_key + end + end + + context "with old schema" do + include_context "encryption key" + include_context "database" + + before do + db.create_table :slosilo_keystore do + String :id, primary_key: true + bytea :key, null: false + end + subject.put_key 'test', key + end + + context "after migration" do + before { subject.migrate! } + + it "supports look up by id" do + expect(subject.get_key("test")).to eq(key) + end + + it "supports look up by fingerprint, without a warning" do + expect($stderr.grab do + expect(subject.get_by_fingerprint(key.fingerprint)).to eq([key, 'test']) + end).to be_empty + end + end + + it "supports look up by id" do + expect(subject.get_key("test")).to eq(key) + end + + it "supports look up by fingerprint, but issues a warning" do + expect($stderr.grab do + expect(subject.get_by_fingerprint(key.fingerprint)).to eq([key, 'test']) + end).not_to be_empty + end + end + + shared_context "current schema" do + include_context "database" + before do + Sequel.extension :migration + require 'slosilo/adapters/sequel_adapter/migration.rb' + Sequel::Migration.descendants.first.apply db, :up + end + end + + context "with current schema" do + include_context "encryption key" + include_context "current schema" + before do + subject.put_key 'test', key + end + + it "supports look up by id" do + expect(subject.get_key("test")).to eq(key) + end + + it "supports look up by fingerprint" do + expect(subject.get_by_fingerprint(key.fingerprint)).to eq([key, 'test']) + end + end + + context "with an encryption key", :wip do + include_context "encryption key" + include_context "current schema" + + it { is_expected.to be_secure } + + it "saves the keys in encrypted form" do + subject.put_key 'test', key + + expect(db[:slosilo_keystore][id: 'test'][:key]).to_not eq(key.to_der) + expect(subject.get_key 'test').to eq(key) + end + end + + context "without an encryption key", :wip do + before do + Slosilo.encryption_key = nil + end + + include_context "current schema" + + it { is_expected.not_to be_secure } + + it "refuses to store a private key" do + expect { subject.put_key 'test', key }.to raise_error(Slosilo::Error::InsecureKeyStorage) + end + + it "saves the keys in plaintext form" do + pkey = key.public + subject.put_key 'test', pkey + + expect(db[:slosilo_keystore][id: 'test'][:key]).to eq(pkey.to_der) + expect(subject.get_key 'test').to eq(pkey) + end + end +end diff --git a/gems/slosilo/spec/slosilo_spec.rb b/gems/slosilo/spec/slosilo_spec.rb new file mode 100644 index 0000000000..38ed63f371 --- /dev/null +++ b/gems/slosilo/spec/slosilo_spec.rb @@ -0,0 +1,124 @@ +require 'spec_helper' + +describe Slosilo do + include_context "with mock adapter" + include_context "with example key" + before { Slosilo['test'] = key } + + describe '[]' do + it "returns a Slosilo::Key" do + expect(Slosilo[:test]).to be_instance_of Slosilo::Key + end + + it "allows looking up by fingerprint" do + expect(Slosilo[fingerprint: key_fingerprint]).to eq(key) + end + + context "when the requested key does not exist" do + it "returns nil instead of creating a new key" do + expect(Slosilo[:aether]).not_to be + end + end + end + + describe '.sign' do + let(:own_key) { double "own key" } + before { allow(Slosilo).to receive(:[]).with(:own).and_return own_key } + let (:argument) { double "thing to sign" } + it "fetches the own key and signs using that" do + expect(own_key).to receive(:sign).with(argument) + Slosilo.sign argument + end + end + + describe '.token_valid?' do + before { allow(adapter['test']).to receive_messages token_valid?: false } + let(:key2) { double "key 2", token_valid?: false } + let(:key3) { double "key 3", token_valid?: false } + before do + adapter[:key2] = key2 + adapter[:key3] = key3 + end + + let(:token) { double "token" } + subject { Slosilo.token_valid? token } + + context "when no key validates the token" do + before { allow(Slosilo::Key).to receive_messages new: (double "key", token_valid?: false) } + it { is_expected.to be_falsey } + end + + context "when a key validates the token" do + let(:valid_key) { double token_valid?: true } + let(:invalid_key) { double token_valid?: true } + before do + allow(Slosilo::Key).to receive_messages new: invalid_key + adapter[:key2] = valid_key + end + + it { is_expected.to be_truthy } + end + end + + describe '.token_signer' do + + context "when token matches a key" do + let(:token) {{ 'data' => 'foo', 'key' => key.fingerprint, 'signature' => 'XXX' }} + + context "and the signature is valid" do + before { allow(key).to receive(:token_valid?).with(token).and_return true } + + it "returns the key id" do + expect(subject.token_signer(token)).to eq('test') + end + end + + context "and the signature is invalid" do + before { allow(key).to receive(:token_valid?).with(token).and_return false } + + it "returns nil" do + expect(subject.token_signer(token)).not_to be + end + end + end + + context "when token doesn't match a key" do + let(:token) {{ 'data' => 'foo', 'key' => "footprint", 'signature' => 'XXX' }} + it "returns nil" do + expect(subject.token_signer(token)).not_to be + end + end + + context "with JWT token" do + before do + expect(key).to receive(:validate_jwt) do |jwt| + expect(jwt.header).to eq 'kid' => key.fingerprint + expect(jwt.claims).to eq({}) + expect(jwt.signature).to eq 'sig' + end + end + + it "accepts pre-parsed JSON serialization" do + expect(Slosilo.token_signer( + 'protected' => 'eyJraWQiOiIxMDdiZGI4NTAxYzQxOWZhZDJmZGIyMGI0NjdkNGQwYTYyYTE2YTk4YzM1ZjJkYTBlYjNiMWZmOTI5Nzk1YWQ5In0=', + 'payload' => 'e30=', + 'signature' => 'c2ln' + )).to eq 'test' + end + + it "accepts pre-parsed JWT token" do + expect(Slosilo.token_signer(Slosilo::JWT( + 'protected' => 'eyJraWQiOiIxMDdiZGI4NTAxYzQxOWZhZDJmZGIyMGI0NjdkNGQwYTYyYTE2YTk4YzM1ZjJkYTBlYjNiMWZmOTI5Nzk1YWQ5In0=', + 'payload' => 'e30=', + 'signature' => 'c2ln' + ))).to eq 'test' + end + + it "accepts compact serialization" do + expect(Slosilo.token_signer( + 'eyJraWQiOiIxMDdiZGI4NTAxYzQxOWZhZDJmZGIyMGI0NjdkNGQwYTYyYTE2YTk4YzM1ZjJkYTBlYjNiMWZmOTI5Nzk1YWQ5In0=.e30=.c2ln' + )).to eq 'test' + end + end + end +end diff --git a/gems/slosilo/spec/spec_helper.rb b/gems/slosilo/spec/spec_helper.rb new file mode 100644 index 0000000000..0530f63f59 --- /dev/null +++ b/gems/slosilo/spec/spec_helper.rb @@ -0,0 +1,84 @@ +require "simplecov" +require "simplecov-cobertura" + +SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter +SimpleCov.start + +require 'slosilo' + +shared_context "with mock adapter" do + require 'slosilo/adapters/mock_adapter' + + let(:adapter) { Slosilo::Adapters::MockAdapter.new } + before { Slosilo::adapter = adapter } +end + +shared_context "with example key" do + let(:rsa) { OpenSSL::PKey::RSA.new """ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAtTG/SQhW9QawP+GL6EZ5Al9gscCr7HiRO7MuQqFkaXIJD6+3 +prdHRrb0qqjNlGFgDBGAuswZ2AYqhBt7eekup+/vIpI5n04b0w+is3WwZAFco4uP +ojDeM0aY65Ar3Zgra2vWUJXRwBumroZjVBVoLJSgVfwIhwU6ORbS2oJflbtqxpuS +zkPDqS6RwEzI/DHuHTOI26fe+vfuDqGOuSR6iVI16lfvTbWwccpDwU0W9vSlyjjD +LIw0MnoKL3DHyzO66s+oNNRleMvjghQtJk/xg1kRuHReJ5/ygt2zyzdKSLeqU+T+ +TCWw/F65jrFElftexiS+g+lZC467VLCaMe1fJQIDAQABAoIBAQCiNWzXRr4CEQDL +z3Deeehu9U+tEZ1Xzv/FgD0TrUQlGc9+2YIBn+YRKkySUxfnk9zWMP0bPQiN2cdK +CQhbNSNteGCOhHVNZjGGm2K+YceNX6K9Tn1BZ5okMTlI+QIsGMQWIK316omh/58S +coCNj7R45H09PKmtpkJfRU1yDHDhqypjPDpb9/7U5mt3g2BdXYi+1hilfonHoDrC +yy3eRdf7Tlij9O3UeM+Z7pZrKATcvpDkYbNWizDITvKMYy6Ss+ajM5v7lt6QN5LP +MHjwX8Ilrxkxl0jeopr4f94tR7rNDZbLC457j8gns7cUeODtF7pPZqlrlk4KOq8Q +DvEMt2ZpAoGBAOLNUiO1SwRo75Y8ukuMVQev8O8WuzEEGINoM1lQiYlbUw3HmVp3 +iUvv58ANmKSzTXpOEZ1L8pZHSp435FrzD2WZmCAoXhNdfAXtmZA7Y46iE6BF4qrr +UegtLPhVgwpO74Y+4w2YwfDknzCOhWE4sxCbukuSvxz2pz1Vm31eFB6jAoGBAMyF +VxfYq9WhmLNsHqR+qfhb1EC5FfpSq23z/o4ryiKqCaWHimVtOO7DL7x2SK3mVNNZ +X8b4+vnJpAQ3nOxeg8fpmBaLAWYRna2AN/CYVIMKYusawhsGAlZZTu2mtJKLiOPS +8/z5dK55xJWlG5JalUB+n/4vd3WmXiT/XJj3qU+XAoGBALyHzLXeKCPcTvzmMj5G +wxAG0xMMJEMUkoP5hGXEKvBBOAMGXpXzM/Ap1s2w/6g5XDhE2SOWVGtTi9WFxI9N +6Qid6vUgWUNjvIr4/WQF2jZgyEu8jDVkM8v6cZ1lB+7zuuwvLnLI/r6ObT3h20H7 +7e3qZawYqkEbT94OYZiPMc5dAoGAHmIQtjIyFOKU1NLTGozWo1bBCXx1j2KIpSUC +RAytUsj/9d9U6Ax50L6ecNkBoxP8tgko+V4zqrgR7a51WYgQ+7nwJikwZAFp80SB +CvUWWQFKALNQ8sLJxhouZ4/Ec6DXDUFhjcthUio00iZdGjjqw1IMYq6aiJfWlJh7 +IR5pwLECgYEAyjlguks/3cjrHRF+yjonxT4tLuBI/n3TAQUPqmtkJtcwZSAJas/1 +c/twlAJ7F6n3ZroF3lgPxMJRRHZl4Z4dJsDatIxVShf3nSGg9Mi5C25obxahbv5/ +Dg1ikwi8GUF4HPZe9DyhXgDhg19wM/qcpjX8bSypsUWHWP+FanhjdWU= +-----END RSA PRIVATE KEY----- + """ } + let (:key) { Slosilo::Key.new rsa.to_der } + let (:key_fingerprint) { "107bdb8501c419fad2fdb20b467d4d0a62a16a98c35f2da0eb3b1ff929795ad9" } + + let (:another_rsa) do + OpenSSL::PKey::RSA.new """ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAryP0uGEIcDFmHDj1MjxbW+eWMeQ1k2FTKI7qx2M3MP9FR3Bz +KjFzGKnAA6QV46K/QtEt+wpWedB/bcikPXY4/vh/b2TEi8Ybw2ztT1oW9le8Djsz +3sQv5QrHsOXzSIARw4NZYxunxMFKCVC9jA8tXJb16RLgS3wAOMiPADlWIKEmPIX6 ++hg2PDgFcrCuL3XAwJ4GKy3Q5BpIFF2j+wRNfjCXDFf1bU9Gy9DND8Y50Khhw/Zn +GYN1Y3AZ3YPzz1SPf08WM663ImYwORjdkA5VlIAMKcmSStNZZUrCOo7DQjNZVD2O +vfGhGUlPqYkmTPnCG2aNP8aJm3IbF+Cb6N6PjwIDAQABAoIBAEaYtr9PlagrsV40 +81kxjR3pptgrhhEHTQ7vNOH0Mz4T16gpQrLCRgOuARE2pgAhDPlw+hjUHPFzQrpN +Ay8nJWhZYHzVYIh67ZwDn1C6HsFjshEGei0UZb3sb3v15O/Xd9GYc4KIlkKwKxjA +K/d18rH8w9kUW8bxj+FTrpjHg9kYkWGjl1WUM4o4dALVVAbbILCHKUIv3wmU5Off +oqBDunItrfVvvc9UOt1SMO15fwuZZpk0B5cjjo6+1NNpIOzqnuu48iI5dQRAIr50 +n44U4/Ix4E1p4i/9i5trCeSZRMrVxBruNxFBtCeDU6YW5fXYNBLptndfb83iqSJf +46myqakCgYEA2MAsbtOcvQv+C7KsRMQih4WqpybV/TRdeC+dZ3flPvSuI8VLJAHp +p2Tp3WXATCwgUWL/iktwWE7WFMn3VvAuMm2ITmAze/Uk71uUS5R+iaGIeRXHgd9J +fyJrIeD63ncWbb23rif2sO6zH4cp9NLS/OopHiRNlRsWEUoGpybxczMCgYEAztrf +mX4oqjqk4af4o4/UHVp3Y9lpcUXRi6dYYECoqv6wS7qCIbJkD4I4P6oTwvk25vbk +p9fwOttuqHC53/rDXVjedNe9VExIe5NhVaug1SyArw/qsafYs0QeDRBkSgCcLfP6 +LP4g824Wbv52X33BO0rJbDCICDqGDCOkqB4XcjUCgYBCkcMTxqo85ZIAxb9i31o7 +hTIEZEkUmyCZ6QXO4WPnEf7pvY52YKACaVvqQ3Xr7yF93YneT40RkiTt/ZmZeeq2 +Ui2q5KDrUT8mxFmnXNQAMTxY8/dyS8Gm6ks8/HwQF0MsMThYpK1/adBZvomER7vF +MaWvPDcXtFnytWmVrMA7QQKBgQDIHpHR4m6e+atIMIPoYR5Z44q7i7tp/ZzTGevy ++rry6wFN0jtRNE9/fYDDftwtdYL7AYKHKu7bUi0FQkFhAi39YhudOJaPNlmtTBEP +m8I2Wh6IvsJUa0jHbbAQ/Xm46kwuXOn8m0LvnuKPMRj+GyBVJ24kf/Mq2suSdO04 +RBx0vQKBgFz93G6bSzmFg0BRTqRWEXEIuYkMIZDe48OjeP4pLYH9aERsL/f/8Dyc +X2nOMv/TdLP7mvGnwCt/sQ2626DdiNqimekyBki9J2r6BzBNVmEvnLAcYaQAiQYz +ooQ2FuL0K6ukQfHPjuMswqi41lmVH8gIVqVC+QnImUCrGxH9WXWy +-----END RSA PRIVATE KEY----- + """ + end + + def self.mock_own_key + before { allow(Slosilo).to receive(:[]).with(:own).and_return key } + end +end diff --git a/gems/slosilo/spec/symmetric_spec.rb b/gems/slosilo/spec/symmetric_spec.rb new file mode 100644 index 0000000000..c3d46862ca --- /dev/null +++ b/gems/slosilo/spec/symmetric_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe Slosilo::Symmetric do + # TODO transform it to class methods only? + let(:plaintext) { "quick brown fox jumped over the lazy dog" } + let(:auth_data) { "some record id" } + let(:key) { "^\xBAIv\xDB1\x0Fi\x04\x11\xFD\x14\xA7\xCD\xDFf\x93\xFE\x93}\v\x01\x11\x98\x14\xE0;\xC1\xE2 v\xA5".force_encoding("ASCII-8BIT") } + let(:iv) { "\xD9\xABn\x01b\xFA\xBD\xC2\xE5\xEA\x01\xAC".force_encoding("ASCII-8BIT") } + let(:ciphertext) { "G^W1\x9C\xD4\xCC\x87\xD3\xFF\x86[\x0E3\xC0\xC8^\xD9\xABn\x01b\xFA\xBD\xC2\xE5\xEA\x01\xAC\x9E\xB9:\xF7\xD4ebeq\xDC \xC0sG\xA4\xAE,\xB8A|\x97\xBC\xFD\x85\xE1\xB93\x95>\xBD\n\x05\xFB\x15\x1F\x06#3M9".force_encoding('ASCII-8BIT') } + + describe '#encrypt' do + it "encrypts with AES-256-GCM" do + allow(subject).to receive_messages random_iv: iv + expect(subject.encrypt(plaintext, key: key, aad: auth_data)).to eq(ciphertext) + end + end + + describe '#decrypt' do + + it "doesn't fail when called by multiple threads" do + threads = [] + + begin + # Verify we can successfuly decrypt using many threads without OpenSSL + # errors. + 1000.times do + threads << Thread.new do + 100.times do + expect( + subject.decrypt(ciphertext, key: key, aad: auth_data) + ).to eq(plaintext) + end + end + end + ensure + threads.each(&:join) + end + end + + it "decrypts with AES-256-GCM" do + expect(subject.decrypt(ciphertext, key: key, aad: auth_data)).to eq(plaintext) + end + + + context "when the ciphertext has been messed with" do + let(:ciphertext) { "pwnd!" } # maybe we should do something more realistic like add some padding? + it "raises an exception" do + expect{ subject.decrypt(ciphertext, key: key, aad: auth_data)}.to raise_exception /Invalid version/ + end + context "by adding a trailing 0" do + let(:new_ciphertext){ ciphertext + '\0' } + it "raises an exception" do + expect{ subject.decrypt(new_ciphertext, key: key, aad: auth_data) }.to raise_exception /Invalid version/ + end + end + end + + context "when no auth_data is given" do + let(:auth_data){""} + let(:ciphertext){ "Gm\xDAT\xE8I\x9F\xB7\xDC\xBB\x84\xD3Q#\x1F\xF4\x8C\aV\x93\x8F_\xC7\xBC87\xC9U\xF1\xAF\x8A\xD62\x1C5H\x86\x17\x19=B~Y*\xBC\x9D\eJeTx\x1F\x02l\t\t\xD3e\xA4\x11\x13y*\x95\x9F\xCD\xC4@\x9C"} + + it "decrypts the message" do + expect(subject.decrypt(ciphertext, key: key, aad: auth_data)).to eq(plaintext) + end + + context "and the ciphertext has been messed with" do + it "raises an exception" do + expect{ subject.decrypt(ciphertext + "\0\0\0", key: key, aad: auth_data)}.to raise_exception OpenSSL::Cipher::CipherError + end + end + end + + context "when the auth data doesn't match" do + let(:auth_data){ "asdf" } + it "raises an exception" do + expect{ subject.decrypt(ciphertext, key: key, aad: auth_data)}.to raise_exception OpenSSL::Cipher::CipherError + end + end + end + + describe '#random_iv' do + it "generates a random iv" do + expect_any_instance_of(OpenSSL::Cipher).to receive(:random_iv).and_return :iv + expect(subject.random_iv).to eq(:iv) + end + end + + describe '#random_key' do + it "generates a random key" do + expect_any_instance_of(OpenSSL::Cipher).to receive(:random_key).and_return :key + expect(subject.random_key).to eq(:key) + end + end +end diff --git a/gems/slosilo/test.sh b/gems/slosilo/test.sh new file mode 100755 index 0000000000..bd6a15484d --- /dev/null +++ b/gems/slosilo/test.sh @@ -0,0 +1,27 @@ +#!/bin/bash -xe + +iid=slosilo-test-$(date +%s) + +docker build -t $iid -f - . << EOF + FROM ruby:3.0 + WORKDIR /app + COPY Gemfile slosilo.gemspec ./ + RUN bundle + COPY . ./ + RUN bundle +EOF + +cidfile=$(mktemp -u) +docker run --cidfile $cidfile -v /app/spec/reports $iid bundle exec rake jenkins || : + +cid=$(cat $cidfile) + +docker cp $cid:/app/spec/reports spec/ +docker cp $cid:/app/coverage spec + +docker rm $cid + +# untag, will use cache next time if available but no junk will be left +docker rmi $iid + +rm $cidfile From d5a4ecfe2ed6935c01f9318ca8a41ba0b70863f3 Mon Sep 17 00:00:00 2001 From: evgenys Date: Wed, 21 Jun 2023 17:35:05 +0300 Subject: [PATCH 2/2] Update jenkinsfile with slosilo new path and slosilo changelog --- Jenkinsfile | 8 ++++++++ gems/slosilo/CHANGELOG.md | 3 +++ 2 files changed, 11 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index 90af3524c5..cbf3c019b1 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -333,6 +333,7 @@ pipeline { spec/reports/*.xml, spec/reports-audit/*.xml, gems/conjur-rack/spec/reports/*.xml, + gems/slosilo/spec/reports/*.xml, cucumber/*/features/reports/**/*.xml ''' ) @@ -893,10 +894,12 @@ pipeline { spec/reports/*.xml, spec/reports-audit/*.xml, gems/conjur-rack/spec/reports/*.xml, + gems/slosilo/spec/reports/*.xml cucumber/*/features/reports/**/*.xml, ee-test/spec/reports/*.xml, ee-test/spec/reports-audit/*.xml, ee-test/gems/conjur-rack/spec/reports/*.xml, + ee-test/gems/slosilo/spec/reports/*.xml, ee-test/cucumber/*/features/reports/**/*.xml ''' ) @@ -1091,6 +1094,11 @@ def conjurTests() { "Rack - ${env.STAGE_NAME}": { sh 'cd gems/conjur-rack && ./test.sh' } + ], + "slosilo": [ + "Slosilo - ${env.STAGE_NAME}": { + sh 'cd gems/slosilo && ./test.sh' + } ] ] } diff --git a/gems/slosilo/CHANGELOG.md b/gems/slosilo/CHANGELOG.md index d287dc93f5..1f9ffbe917 100644 --- a/gems/slosilo/CHANGELOG.md +++ b/gems/slosilo/CHANGELOG.md @@ -1,3 +1,6 @@ +**This Gem has been moved into Conjur. All Slosilo Changelog entries should +appear in the main Changelog.** + # v3.0.1 * The symmetric cipher class now encrypts and decrypts in a thread-safe manner.