diff --git a/lib/diaspora_federation/entities.rb b/lib/diaspora_federation/entities.rb index 6760c56..11c0326 100644 --- a/lib/diaspora_federation/entities.rb +++ b/lib/diaspora_federation/entities.rb @@ -12,6 +12,7 @@ require "diaspora_federation/entities/related_entity" # abstract types require "diaspora_federation/entities/post" +require "diaspora_federation/entities/signable" require "diaspora_federation/entities/relayable" # types diff --git a/lib/diaspora_federation/entities/relayable.rb b/lib/diaspora_federation/entities/relayable.rb index 4ed432f..b293e7c 100644 --- a/lib/diaspora_federation/entities/relayable.rb +++ b/lib/diaspora_federation/entities/relayable.rb @@ -5,10 +5,7 @@ module DiasporaFederation # has a parent, identified by guid. Relayables are also signed and signing/verification # logic is embedded into Salmon XML processing code. module Relayable - include Logging - - # Digest instance used for signing - DIGEST = OpenSSL::Digest::SHA256.new + include Signable # Order from the parsed xml for signature # @return [Array] order from xml @@ -107,24 +104,6 @@ module DiasporaFederation private - # Check that signature is a correct signature - # - # @param [String] author The author of the signature - # @param [String] signature_key The signature to be verified - # @return [Boolean] signature valid - def verify_signature(author, signature_key) - pubkey = DiasporaFederation.callbacks.trigger(:fetch_public_key, author) - raise PublicKeyNotFound, "signature=#{signature_key} person=#{author} obj=#{self}" if pubkey.nil? - - signature = public_send(signature_key) - raise SignatureVerificationFailed, "no #{signature_key} for #{self}" if signature.nil? - - valid = pubkey.verify(DIGEST, Base64.decode64(signature), signature_data) - raise SignatureVerificationFailed, "wrong #{signature_key} for #{self}" unless valid - - logger.info "event=verify_signature signature=#{signature_key} status=valid obj=#{self}" - end - # Sign with author key # @raise [AuthorPrivateKeyNotFound] if the author private key is not found # @return [String] A Base64 encoded signature of #signature_data with key @@ -147,14 +126,6 @@ module DiasporaFederation end end - # Sign the data with the key - # - # @param [OpenSSL::PKey::RSA] privkey An RSA key - # @return [String] A Base64 encoded signature of #signature_data with key - def sign_with_key(privkey) - Base64.strict_encode64(privkey.sign(DIGEST, signature_data)) - end - # Update the signatures with the keys of the author and the parent # if the signatures are not there yet and if the keys are available. # @@ -245,14 +216,6 @@ module DiasporaFederation # Raised, if creating the author_signature fails, because the private key was not found class AuthorPrivateKeyNotFound < RuntimeError end - - # Raised, if verify_signatures fails to verify signatures (no public key found) - class PublicKeyNotFound < RuntimeError - end - - # Raised, if verify_signatures fails to verify signatures (signatures are wrong) - class SignatureVerificationFailed < RuntimeError - end end end end diff --git a/lib/diaspora_federation/entities/signable.rb b/lib/diaspora_federation/entities/signable.rb new file mode 100644 index 0000000..e020452 --- /dev/null +++ b/lib/diaspora_federation/entities/signable.rb @@ -0,0 +1,53 @@ +module DiasporaFederation + module Entities + # Signable is a module that encapsulates basic signature generation/verification flow for entities. + module Signable + include Logging + + # Digest instance used for signing + DIGEST = OpenSSL::Digest::SHA256.new + + # Sign the data with the key + # + # @param [OpenSSL::PKey::RSA] privkey An RSA key + # @return [String] A Base64 encoded signature of #signature_data with key + def sign_with_key(privkey) + Base64.strict_encode64(privkey.sign(DIGEST, signature_data)) + end + + # Check that signature is a correct signature + # + # @param [String] author The author of the signature + # @param [String] signature_key The signature to be verified + # @return [Boolean] signature valid + def verify_signature(author, signature_key) + pubkey = DiasporaFederation.callbacks.trigger(:fetch_public_key, author) + raise PublicKeyNotFound, "signature=#{signature_key} person=#{author} obj=#{self}" if pubkey.nil? + + signature = public_send(signature_key) + raise SignatureVerificationFailed, "no #{signature_key} for #{self}" if signature.nil? + + valid = pubkey.verify(DIGEST, Base64.decode64(signature), signature_data) + raise SignatureVerificationFailed, "wrong #{signature_key} for #{self}" unless valid + + logger.info "event=verify_signature signature=#{signature_key} status=valid obj=#{self}" + end + + # This method defines what data is used for a signature creation/verification + # + # @abstract + # @return [String] a string to sign + def signature_data + raise NotImplementedError.new("you must override this method to define signature base string") + end + + # Raised, if verify_signatures fails to verify signatures (no public key found) + class PublicKeyNotFound < RuntimeError + end + + # Raised, if verify_signatures fails to verify signatures (signatures are wrong) + class SignatureVerificationFailed < RuntimeError + end + end + end +end diff --git a/spec/lib/diaspora_federation/entities/relayable_spec.rb b/spec/lib/diaspora_federation/entities/relayable_spec.rb index 9161fc7..004d25e 100644 --- a/spec/lib/diaspora_federation/entities/relayable_spec.rb +++ b/spec/lib/diaspora_federation/entities/relayable_spec.rb @@ -15,14 +15,6 @@ module DiasporaFederation let(:legacy_signature_data) { "#{guid};#{author};#{property};#{parent_guid}" } - def sign_with_key(privkey, signature_data) - Base64.strict_encode64(privkey.sign(OpenSSL::Digest::SHA256.new, signature_data)) - end - - def verify_signature(pubkey, signature, signed_string) - pubkey.verify(OpenSSL::Digest::SHA256.new, Base64.decode64(signature), signed_string) - end - describe "#initialize" do it "filters signatures from order" do xml_order = [:author, :guid, :parent_guid, :property, "new_property", :author_signature] diff --git a/spec/lib/diaspora_federation/entities/signable_spec.rb b/spec/lib/diaspora_federation/entities/signable_spec.rb new file mode 100644 index 0000000..bcb31c3 --- /dev/null +++ b/spec/lib/diaspora_federation/entities/signable_spec.rb @@ -0,0 +1,77 @@ +module DiasporaFederation + describe Entities::Signable do + TEST_STRING_VALUE = "abc123".freeze + let(:private_key) { OpenSSL::PKey::RSA.generate(1024) } + let(:test_string) { TEST_STRING_VALUE } + let(:test_signature) { sign_with_key(private_key, test_string) } + + class TestSignableEntity < Entity + include Entities::Signable + + property :my_signature, :string, default: nil + + def signature_data + TEST_STRING_VALUE + end + end + + describe "#signature_data" do + it "raises NotImplementedError when not overridden" do + class TestEntity < Entity + include Entities::Signable + end + + expect { + TestEntity.new({}).signature_data + }.to raise_error(NotImplementedError) + end + end + + describe "#sign_with_key" do + it "produces a correct signature" do + signature = TestSignableEntity.new({}).sign_with_key(private_key) + expect(verify_signature(private_key.public_key, signature, test_string)).to be_truthy + end + end + + describe "#verify_signature" do + it "doesn't raise if signature is correct" do + expect_callback(:fetch_public_key, "id@example.tld").and_return(private_key.public_key) + + expect { + TestSignableEntity + .new(my_signature: test_signature) + .verify_signature("id@example.tld", :my_signature) + }.not_to raise_error + end + + it "raises PublicKeyNotFound when key isn't provided" do + expect_callback(:fetch_public_key, "id@example.tld").and_return(nil) + + expect { + TestSignableEntity + .new(my_signature: test_signature) + .verify_signature("id@example.tld", :my_signature) + }.to raise_error(Entities::Signable::PublicKeyNotFound) + end + + it "raises SignatureVerificationFailed when signature isn't provided" do + expect_callback(:fetch_public_key, "id@example.tld").and_return(private_key.public_key) + + expect { + TestSignableEntity.new({}).verify_signature("id@example.tld", :my_signature) + }.to raise_error(Entities::Signable::SignatureVerificationFailed) + end + + it "raises SignatureVerificationFailed when signature is wrong" do + expect_callback(:fetch_public_key, "id@example.tld").and_return(private_key.public_key) + + expect { + TestSignableEntity + .new(my_signature: "faked signature") + .verify_signature("id@example.tld", :my_signature) + }.to raise_error(Entities::Signable::SignatureVerificationFailed) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9f07f48..c5f7454 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -45,6 +45,14 @@ def add_signatures(hash, klass=described_class) hash[:parent_author_signature] = properties[:parent_author_signature] end +def sign_with_key(privkey, signature_data) + Base64.strict_encode64(privkey.sign(OpenSSL::Digest::SHA256.new, signature_data)) +end + +def verify_signature(pubkey, signature, signed_string) + pubkey.verify(OpenSSL::Digest::SHA256.new, Base64.decode64(signature), signed_string) +end + # Requires supporting files with custom matchers and macros, etc, # in ./support/ and its subdirectories. fixture_builder_file = "#{File.dirname(__FILE__)}/support/fixture_builder.rb"