From de1f7f88f66372c20d92a7d9b03b9392f29bf9da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Sun, 12 Oct 2014 16:53:07 +0200 Subject: [PATCH] Remove (insecure, deprecated) signed http request protocol --- lib/slosilo.rb | 2 - lib/slosilo/http_request.rb | 59 ---------------- lib/slosilo/rack/middleware.rb | 123 --------------------------------- lib/slosilo/version.rb | 2 +- spec/http_request_spec.rb | 107 ---------------------------- spec/http_stack_spec.rb | 44 ------------ spec/rack_middleware_spec.rb | 109 ----------------------------- 7 files changed, 1 insertion(+), 445 deletions(-) delete mode 100644 lib/slosilo/http_request.rb delete mode 100644 lib/slosilo/rack/middleware.rb delete mode 100644 spec/http_request_spec.rb delete mode 100644 spec/http_stack_spec.rb delete mode 100644 spec/rack_middleware_spec.rb diff --git a/lib/slosilo.rb b/lib/slosilo.rb index 2d4ab80..385f65b 100644 --- a/lib/slosilo.rb +++ b/lib/slosilo.rb @@ -3,8 +3,6 @@ require "slosilo/symmetric" require "slosilo/attr_encrypted" require "slosilo/random" -require "slosilo/rack/middleware" -require "slosilo/http_request" require "slosilo/errors" if defined? Sequel diff --git a/lib/slosilo/http_request.rb b/lib/slosilo/http_request.rb deleted file mode 100644 index 75cccfb..0000000 --- a/lib/slosilo/http_request.rb +++ /dev/null @@ -1,59 +0,0 @@ -module Slosilo - # A mixin module which simplifies generating signed and encrypted requests. - # It's designed to be mixed into a standard Net::HTTPRequest object - # and ensures the request is signed and optionally encrypted before execution. - # Requests prepared this way will be recognized by Slosilo::Rack::Middleware. - # - # As an example, you can use it with RestClient like so: - # RestClient.add_before_execution_proc do |req, params| - # require 'slosilo' - # req.extend Slosilo::HTTPRequest - # req.keyname = :somekey - # end - # - # The request won't be encrypted unless you set the destination keyname. - - module HTTPRequest - # Encrypt the request with key named @keyname from Slosilo::Keystore. - # If calling this manually, make sure to encrypt before signing. - def encrypt! - return unless @keyname - return unless body && !body.empty? - self.body, key = Slosilo[@keyname].encrypt body - self['X-Slosilo-Key'] = Base64::urlsafe_encode64 key - end - - # Sign the request with :own key from Slosilo::Keystore. - # If calling this manually, make sure to encrypt before signing. - def sign! - token = Slosilo[:own].signed_token signed_data - self['Timestamp'] = token["timestamp"] - self['X-Slosilo-Signature'] = token["signature"] - end - - # Build the data hash to sign. - def signed_data - data = { "path" => path, "body" => [body].pack('m0') } - if key = self['X-Slosilo-Key'] - data["key"] = key - end - if authz = self['Authorization'] - data["authorization"] = authz - end - data - end - - # Encrypt, sign and execute the request. - def exec *a - # we need to hook here because the body might be set - # in several ways and here it's hopefully finalized - encrypt! - sign! - super *a - end - - # Name of the key used to encrypt the request. - # Use it to establish the identity of the receiver. - attr_accessor :keyname - end -end diff --git a/lib/slosilo/rack/middleware.rb b/lib/slosilo/rack/middleware.rb deleted file mode 100644 index 8e38124..0000000 --- a/lib/slosilo/rack/middleware.rb +++ /dev/null @@ -1,123 +0,0 @@ -module Slosilo - module Rack - # Con perform verification of request signature and decryption of request body. - # - # Signature verification and body decryption are enabled with constructor switches and are - # therefore performed (or not) for all requests. - # - # When signature verification is performed, the following elements are included in the - # signature string: - # - # 1. Request path and query string - # 2. base64 encoded request body - # 3. Request timestamp from HTTP_TIMESTAMP - # 4. Body encryption key from HTTP_X_SLOSILO_KEY (if present) - # - # When body decryption is performed, an encryption key for the message body is encrypted - # with this service's public key and placed in HTTP_X_SLOSILO_KEY. This middleware - # decryps the key using our :own private key, and then decrypts the body using the decrypted key. - class Middleware - class EncryptionError < SecurityError - end - class SignatureError < SecurityError - end - - def initialize app, opts = {} - @app = app - @encryption_required = opts[:encryption_required] || false - @signature_required = opts[:signature_required] || false - end - - def call env - @env = env - @body = env['rack.input'].read rescue "" - - begin - verify - decrypt - rescue EncryptionError - return error 403, $!.message - rescue SignatureError - return error 401, $!.message - end - - @app.call env - end - - private - def verify - if signature - raise SignatureError, "Bad signature" unless Slosilo.token_valid?(token) - else - raise SignatureError, "Signature required" if signature_required? - end - end - - attr_reader :env - - def token - return nil unless signature - t = { "data" => { "path" => path, "body" => [body].pack('m0') }, "timestamp" => timestamp, "signature" => signature } - t["data"]["key"] = encoded_key if encoded_key - t['data']['authorization'] = env['HTTP_AUTHORIZATION'] if env['HTTP_AUTHORIZATION'] - t - end - - def path - env['SCRIPT_NAME'] + env['PATH_INFO'] + query_string - end - - def query_string - if env['QUERY_STRING'].empty? - '' - else - '?' + env['QUERY_STRING'] - end - end - - attr_reader :body - - def timestamp - env['HTTP_TIMESTAMP'] - end - - def signature - env['HTTP_X_SLOSILO_SIGNATURE'] - end - - def encoded_key - env['HTTP_X_SLOSILO_KEY'] - end - - def key - if encoded_key - Base64::urlsafe_decode64(encoded_key) - else - raise EncryptionError, "Encryption required" if encryption_required? - end - end - - def decrypt - return unless key - plaintext = Slosilo[:own].decrypt body, key - env['rack.input'] = StringIO.new plaintext - rescue EncryptionError - raise unless body.empty? || body.nil? - rescue Exception => e - raise EncryptionError, "Bad encryption", e.backtrace - end - - def error status, message - [status, { 'Content-Type' => 'text/plain', 'Content-Length' => message.length.to_s }, [message] ] - end - - def encryption_required? - @encryption_required - end - - def signature_required? - @signature_required - end - end - end -end diff --git a/lib/slosilo/version.rb b/lib/slosilo/version.rb index 8a33196..8f5904f 100644 --- a/lib/slosilo/version.rb +++ b/lib/slosilo/version.rb @@ -1,3 +1,3 @@ module Slosilo - VERSION = "0.5.0" + VERSION = "1.0.0" end diff --git a/spec/http_request_spec.rb b/spec/http_request_spec.rb deleted file mode 100644 index 367cbe0..0000000 --- a/spec/http_request_spec.rb +++ /dev/null @@ -1,107 +0,0 @@ -require 'spec_helper' - -describe Slosilo::HTTPRequest do - let(:keyname) { :bacon } - let(:encrypt) { subject.encrypt! } - subject { Hash.new } - before do - subject.extend Slosilo::HTTPRequest - subject.keyname = keyname - end - - describe "#sign!" do - let(:own_key) { double "own key" } - before { Slosilo.stub(:[]).with(:own).and_return own_key } - - let(:signed_data) { "this is the truest truth" } - before { subject.stub signed_data: signed_data } - let(:timestamp) { "long time ago" } - let(:signature) { "seal of approval" } - let(:token) { { "data" => signed_data, "timestamp" => timestamp, "signature" => signature } } - - it "makes a token out of the data to sign and inserts headers" do - own_key.stub(:signed_token).with(signed_data).and_return token - subject.should_receive(:[]=).with 'Timestamp', timestamp - subject.should_receive(:[]=).with 'X-Slosilo-Signature', signature - subject.sign! - end - end - - describe "#signed_data" do - before { subject.stub path: :path, body: 'body' } - context "when X-Slosilo-Key not present" do - its(:signed_data) { should == { "path" => :path, "body" => "Ym9keQ==" } } - end - - context "when X-Slosilo-Key is present" do - before { subject.merge! 'X-Slosilo-Key' => :key } - its(:signed_data) { should == { "path" => :path, "body" => "Ym9keQ==", "key" => :key } } - end - end - - describe "#encrypt!" do - context "when key not set" do - before { subject.keyname = nil } - it "does nothing" do - subject.should_not_receive(:body=) - encrypt - end - end - - context "when requested key does not exist" do - before { Slosilo.stub(:[]).and_return nil } - it "raises error" do - expect{ encrypt }.to raise_error - end - end - - context "when the key exists" do - let(:key) { double "key" } - context "when the body is not empty" do - let(:plaintext) { "Keep your solutions close, and your problems closer." } - let(:ciphertext) { "And, when you want something, all the universe conspires in helping you to achieve it." } - let(:skey) { "make me sound like a fool instead" } - before do - subject.stub body: plaintext - key.stub(:encrypt).with(plaintext).and_return([ciphertext, skey]) - Slosilo.stub(:[]).with(keyname).and_return key - end - - it "encrypts the message body and adds the X-Slosilo-Key header" do - subject.should_receive(:body=).with ciphertext - subject.should_receive(:[]=).with 'X-Slosilo-Key', Base64::urlsafe_encode64(skey) - encrypt - end - end - - context "when the body is empty" do - before { subject.stub body: "" } - it "doesn't set the key header" do - subject.should_not_receive(:[]=).with 'X-Slosilo-Key' - encrypt - end - end - end - end - - describe "#exec" do - class Subject - def exec *a - "ok, got it" - end - - def initialize keyname - extend Slosilo::HTTPRequest - self.keyname = keyname - end - end - - subject { Subject.new keyname } - - it "encrypts, then signs and delegates to the superclass" do - subject.should_receive(:encrypt!).once.ordered - subject.should_receive(:sign!).once.ordered - subject.exec(:foo).should == "ok, got it" - end - end -end diff --git a/spec/http_stack_spec.rb b/spec/http_stack_spec.rb deleted file mode 100644 index 3bca312..0000000 --- a/spec/http_stack_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -require 'spec_helper' - -describe "http request stack" do - include_context "with example key" - include_context "with mock adapter" - before { Slosilo[:own] = key } - - class MockRequest < Hash - def exec *a - end - - def [] name - name = name.sub(/^HTTP_/,'').gsub('_', '-').split(/(\W)/).map(&:capitalize).join - result = super name - end - - def initialize - extend Slosilo::HTTPRequest - self['Authorization'] = "Simon says it's fine" - end - end - - subject { MockRequest.new } - let(:path) { '/some/path' } - - context "with authorization header" do - it "works" do - mw = Slosilo::Rack::Middleware.new lambda{|_|:ok}, signature_required: true - subject.stub path: path, body: '' - mw.stub path: path - subject.send :exec - mw.call(subject).should == :ok - end - - it "detects tampering" do - mw = Slosilo::Rack::Middleware.new lambda{|_|:ok}, signature_required: true - subject.stub path: path, body: '' - mw.stub path: path - subject.send :exec - subject['Authorization'] = "Simon changed his mind" - mw.call(subject).should_not == :ok - end - end -end diff --git a/spec/rack_middleware_spec.rb b/spec/rack_middleware_spec.rb deleted file mode 100644 index 9df6bfd..0000000 --- a/spec/rack_middleware_spec.rb +++ /dev/null @@ -1,109 +0,0 @@ -require 'spec_helper' - -describe Slosilo::Rack::Middleware do - include_context "with example key" - mock_own_key - - let(:app) { double "app" } - subject { Slosilo::Rack::Middleware.new app } - - describe '#path' do - context "when QUERY_STRING is empty" do - let(:env) { { 'SCRIPT_NAME' => '/foo', 'PATH_INFO' => '/bar', 'QUERY_STRING' => '' } } - before { subject.stub env: env } - its(:path) { should == '/foo/bar' } - end - context "when QUERY_STRING is not" do - let(:env) { { 'SCRIPT_NAME' => '/foo', 'PATH_INFO' => '/bar', 'QUERY_STRING' => 'baz' } } - before { subject.stub env: env } - its(:path) { should == '/foo/bar?baz' } - end - end - - describe '#call' do - let(:call) { subject.call(env) } - let(:path) { "/this/is/the/path" } - before { subject.stub path: path } - context "when no X-Slosilo-Key is given" do - let(:env) { {} } - let(:result) { double "result" } - it "passes the env verbatim" do - app.should_receive(:call).with(env).and_return(result) - call.should == result - end - - context "and X-Slosilo-Signature is given" do - let(:body) { "the body" } - let(:timestamp) { "long time ago" } - let(:signature) { "in blood" } - let(:env) { {'rack.input' => StringIO.new(body), 'HTTP_TIMESTAMP' => timestamp, 'HTTP_X_SLOSILO_SIGNATURE' => signature } } - let(:token) { { "data" => { "path" => path, "body" => "dGhlIGJvZHk=" }, "timestamp" => timestamp, "signature" => signature } } - context "when the signature is valid" do - before { Slosilo.stub(:token_valid?).with(token).and_return true } - it "passes the env verbatim" do - app.should_receive(:call).with(env).and_return(result) - call.should == result - end - end - context "when the signature is invalid" do - before { Slosilo.stub(:token_valid?).with(token).and_return false } - it "returns 401" do - call[0].should == 401 - end - end - end - - context "but encryption is required" do - subject { Slosilo::Rack::Middleware.new app, encryption_required: true } - context "and the body is not empty" do - let(:env) { {'rack.input' => StringIO.new('foo') } } - it "returns 403" do - status, headers, body = call - status.should == 403 - end - end - context "but the body is empty" do - subject { Slosilo::Rack::Middleware.new app, encryption_required: true, signature_required: false } - let(:body) { "" } - let(:timestamp) { "long time ago" } - let(:env) { {'rack.input' => StringIO.new(body) } } - it "passes the env verbatim" do - app.should_receive(:call).with(env).and_return(result) - call.should == result - end - end - end - end - - context "when no X-Slosilo-Signature is given" do - context "but signature is required" do - let(:env) {{}} - subject { Slosilo::Rack::Middleware.new app, signature_required: true } - it "returns 401" do - status, headers, body = call - status.should == 401 - end - end - end - - let(:plaintext) { "If you were taught that elves caused rain, every time it rained, you'd see the proof of elves." } - let(:skey) { "Eiho7xIoFj-Qwqc0swcQQJzJyM1sSv_b6VdRIoHCPRUwemB0v5MNyOirU_5dQ_bNzlmSlo8HDvfAnMgapwpIBH__uDUV_3nCkzrzQVV3-bSp6owJnqebeSQxJMoVMKEWqqek3ZCBPo0OB63A8mkYGu9955gDEDOnlxLkETGb3SmDQIVJtiMmAkUWN0fh9z1M9Ycw9FfworaHKQXRLw6z6Rl-Yoe_TDaiKVlGIYjQKpCz8h_I5lRdrhPJaP53d0yQuKMK3PBHMzE77IikZyQ3VZdoqI9XqzUJF27KehxJ_BCx0oAcPaxG6I7WWe3Xb7K7MhE4HgzqVZACDLhYfm_0XA==" } - let(:ciphertext) { "0\xDE\xE1\xBA=\x06+K\xE0\xCAD\xC6\xE3 d\xC7kx\x90\r\ni\xDCXmS!EP\xAB\xEF\xAA\x13{\x85f\x8FU,\xB3zO\x1F\x85\f\x0E\xAE\xF8\x10`\x1C\x94\xAB@\xFA\xBC\xC0/\x1F\xA6nX\xFF-m\xF4\xC3f\xBB\xCA\x05\xC82\x18l\xC3\xF0v\x96\v\x8F\xFC\xB2\xC7wX;\xF6v\xDCX:\xCC\xF8\xD7\x99\xC8\x1A\xBA\x9F\xDB\xE7\x0F\xF2\xC9f\aaGs\xEFc" } - context "when X-Slosilo-Key is given" do - context "when the key decrypts cleanly" do - let(:env) { {'HTTP_X_SLOSILO_KEY' => skey, 'rack.input' => StringIO.new(ciphertext) } } - it "passes the decrypted contents" do - app.should_receive(:call).with(rack_environment_with_input(plaintext)).and_return(:result) - call.should == :result - end - end - context "when the key is invalid" do - let(:env) { {'HTTP_X_SLOSILO_KEY' => "broken #{skey}", 'rack.input' => StringIO.new(ciphertext) } } - it "returns 403 status" do - status, headers, body = call - status.should == 403 - end - end - end - end -end