refactoring relayable signature-checking

verify signature after creating the entity instance
This commit is contained in:
Benjamin Neff 2016-01-21 04:25:28 +01:00
parent 583d567d67
commit adf14283e3
14 changed files with 103 additions and 111 deletions

View file

@ -21,6 +21,12 @@ module DiasporaFederation
# @see Person#diaspora_id # @see Person#diaspora_id
# @return [String] diaspora ID # @return [String] diaspora ID
property :diaspora_id, xml_name: :diaspora_handle property :diaspora_id, xml_name: :diaspora_handle
# The {Comment} parent is a Post
# @return [String] target type
def target_type
"Post"
end
end end
end end
end end

View file

@ -35,8 +35,8 @@ module DiasporaFederation
property :conversation_guid property :conversation_guid
# The {Message} parent is a {Conversation} # The {Message} parent is a {Conversation}
# @return [String] parent entity type # @return [String] target type
def self.get_target_entity_type(*) def target_type
"Conversation" "Conversation"
end end
end end

View file

@ -25,8 +25,8 @@ module DiasporaFederation
property :poll_answer_guid property :poll_answer_guid
# The {PollParticipation} parent is a {Poll} # The {PollParticipation} parent is a {Poll}
# @return [String] parent entity type # @return [String] target type
def self.get_target_entity_type(*) def target_type
"Poll" "Poll"
end end
end end

View file

@ -31,11 +31,25 @@ module DiasporaFederation
property :parent_guid property :parent_guid
property :parent_author_signature, default: nil property :parent_author_signature, default: nil
property :author_signature, default: nil property :author_signature, default: nil
end
end
# get the type of the parent entity # Adds signatures to the hash with the keys of the author and the parent
# @return [String] parent entity type # if the signatures are not in the hash yet and if the keys are available.
def self.get_target_entity_type(data) #
data[:target_type] || "Post" # @return [Hash] entity data hash with updated signatures
def to_signed_h
to_h.tap do |hash|
if author_signature.nil?
privkey = DiasporaFederation.callbacks.trigger(:fetch_private_key_by_diaspora_id, diaspora_id)
hash[:author_signature] = Signing.sign_with_key(hash, privkey) unless privkey.nil?
end
if parent_author_signature.nil?
privkey = DiasporaFederation.callbacks.trigger(
:fetch_author_private_key_by_entity_guid, target_type, parent_guid
)
hash[:parent_author_signature] = Signing.sign_with_key(hash, privkey) unless privkey.nil?
end end
end end
end end
@ -45,9 +59,7 @@ module DiasporaFederation
# @return [Nokogiri::XML::Element] root element containing properties as child elements # @return [Nokogiri::XML::Element] root element containing properties as child elements
def to_xml def to_xml
entity_xml.tap do |xml| entity_xml.tap do |xml|
hash = to_h hash = to_signed_h
Relayable.update_signatures!(hash, self.class)
xml.at_xpath("author_signature").content = hash[:author_signature] xml.at_xpath("author_signature").content = hash[:author_signature]
xml.at_xpath("parent_author_signature").content = hash[:parent_author_signature] xml.at_xpath("parent_author_signature").content = hash[:parent_author_signature]
end end
@ -58,61 +70,29 @@ module DiasporaFederation
end end
# verifies the signatures (+author_signature+ and +parent_author_signature+ if needed) # verifies the signatures (+author_signature+ and +parent_author_signature+ if needed)
# @param [Hash] data hash with data to verify
# @param [Class] klass entity type
# @raise [SignatureVerificationFailed] if the signature is not valid or no public key is found # @raise [SignatureVerificationFailed] if the signature is not valid or no public key is found
def self.verify_signatures(data, klass) def verify_signatures
pubkey = DiasporaFederation.callbacks.trigger(:fetch_public_key_by_diaspora_id, data[:diaspora_id]) pubkey = DiasporaFederation.callbacks.trigger(:fetch_public_key_by_diaspora_id, diaspora_id)
raise SignatureVerificationFailed, "failed to fetch public key for #{data[:diaspora_id]}" if pubkey.nil? raise SignatureVerificationFailed, "failed to fetch public key for #{diaspora_id}" if pubkey.nil?
raise SignatureVerificationFailed, "wrong author_signature" unless Signing.verify_signature( raise SignatureVerificationFailed, "wrong author_signature" unless Signing.verify_signature(
data, data[:author_signature], pubkey data, author_signature, pubkey
) )
author_is_local = DiasporaFederation.callbacks.trigger( author_is_local = DiasporaFederation.callbacks.trigger(:entity_author_is_local?, target_type, parent_guid)
:entity_author_is_local?, verify_parent_signature unless author_is_local
klass.get_target_entity_type(data),
data[:parent_guid]
)
verify_parent_signature(data, klass) unless author_is_local
end end
private
# this happens only on downstream federation # this happens only on downstream federation
# @param [Hash] data hash with data to verify def verify_parent_signature
# @param [Class] klass entity type pubkey = DiasporaFederation.callbacks.trigger(:fetch_author_public_key_by_entity_guid, target_type, parent_guid)
def self.verify_parent_signature(data, klass)
pubkey = DiasporaFederation.callbacks.trigger( raise SignatureVerificationFailed, "failed to fetch public key for author of #{parent_guid}" if pubkey.nil?
:fetch_author_public_key_by_entity_guid,
klass.get_target_entity_type(data),
data[:parent_guid]
)
raise SignatureVerificationFailed,
"failed to fetch public key for author of #{data[:parent_guid]}" if pubkey.nil?
raise SignatureVerificationFailed, "wrong parent_author_signature" unless Signing.verify_signature( raise SignatureVerificationFailed, "wrong parent_author_signature" unless Signing.verify_signature(
data, data[:parent_author_signature], pubkey data, parent_author_signature, pubkey
) )
end end
private_class_method :verify_parent_signature
# Adds signatures to a given hash with the keys of the author and the parent
# if the signatures are not in the hash yet and if the keys are available.
#
# @param [Hash] data hash given for a signing
# @param [Class] klass entity type
def self.update_signatures!(data, klass)
if data[:author_signature].nil?
privkey = DiasporaFederation.callbacks.trigger(:fetch_private_key_by_diaspora_id, data[:diaspora_id])
data[:author_signature] = Signing.sign_with_key(data, privkey) unless privkey.nil?
end
if data[:parent_author_signature].nil?
privkey = DiasporaFederation.callbacks.trigger(
:fetch_author_private_key_by_entity_guid,
klass.get_target_entity_type(data),
data[:parent_guid]
)
data[:parent_author_signature] = Signing.sign_with_key(data, privkey) unless privkey.nil?
end
end
end end
end end
end end

View file

@ -35,6 +35,10 @@ module DiasporaFederation
class Entity class Entity
extend PropertiesDSL extend PropertiesDSL
# the original data hash with which the entity was created
# @return [Hash] original data
attr_reader :data
# Initializes the Entity with the given attribute hash and freezes the created # Initializes the Entity with the given attribute hash and freezes the created
# instance it returns. # instance it returns.
# #
@ -56,6 +60,8 @@ module DiasporaFederation
raise ArgumentError, "missing required properties: #{missing_props.join(', ')}" raise ArgumentError, "missing required properties: #{missing_props.join(', ')}"
end end
@data = data
self.class.default_values.merge(data).each do |k, v| self.class.default_values.merge(data).each do |k, v|
instance_variable_set("@#{k}", nilify(v)) if setable?(k, v) instance_variable_set("@#{k}", nilify(v)) if setable?(k, v)
end end

View file

@ -97,11 +97,9 @@ module DiasporaFederation
end end
}] }]
if klass.included_modules.include?(Entities::Relayable) klass.new(data).tap do |entity|
Entities::Relayable.verify_signatures(data, klass) entity.verify_signatures if entity.respond_to? :verify_signatures
end end
klass.new(data)
end end
private_class_method :populate_entity private_class_method :populate_entity

View file

@ -21,10 +21,7 @@ module DiasporaFederation
# @param [Symbol] factory_name the factory to generate attributes for (normally entity name) # @param [Symbol] factory_name the factory to generate attributes for (normally entity name)
# @return [Hash] hash with correct signatures # @return [Hash] hash with correct signatures
def self.relayable_attributes_with_signatures(factory_name) def self.relayable_attributes_with_signatures(factory_name)
klass = FactoryGirl.factory_by_name(factory_name).build_class FactoryGirl.build(factory_name).to_signed_h
sort_hash(FactoryGirl.attributes_for(factory_name), klass).tap do |data|
DiasporaFederation::Entities::Relayable.update_signatures!(data, klass)
end
end end
# Generates attributes for signed retraction entity constructor with correct signatures in it # Generates attributes for signed retraction entity constructor with correct signatures in it

View file

@ -21,9 +21,9 @@ XML
it_behaves_like "a relayable Entity" it_behaves_like "a relayable Entity"
describe ".get_target_entity_type" do describe "#target_type" do
it "returns \"Post\" as target type" do it "returns \"Post\" as target type" do
expect(described_class.get_target_entity_type(data)).to eq("Post") expect(described_class.new(data).target_type).to eq("Post")
end end
end end
end end

View file

@ -22,9 +22,9 @@ XML
it_behaves_like "a relayable Entity" it_behaves_like "a relayable Entity"
describe ".get_target_entity_type" do describe "#target_type" do
it "returns data[:target_type] as target type" do it "returns data[:target_type] as target type" do
expect(described_class.get_target_entity_type(data)).to eq(data[:target_type]) expect(described_class.new(data).target_type).to eq(data[:target_type])
end end
end end
end end

View file

@ -23,9 +23,9 @@ XML
it_behaves_like "a relayable Entity" it_behaves_like "a relayable Entity"
describe ".get_target_entity_type" do describe "#target_type" do
it "returns \"Conversation\" as target type" do it "returns \"Conversation\" as target type" do
expect(described_class.get_target_entity_type(data)).to eq("Conversation") expect(described_class.new(data).target_type).to eq("Conversation")
end end
end end
end end

View file

@ -21,9 +21,9 @@ XML
it_behaves_like "a relayable Entity" it_behaves_like "a relayable Entity"
describe ".get_target_entity_type" do describe "#target_type" do
it "returns data[:target_type] as target type" do it "returns data[:target_type] as target type" do
expect(described_class.get_target_entity_type(data)).to eq(data[:target_type]) expect(described_class.new(data).target_type).to eq(data[:target_type])
end end
end end
end end

View file

@ -21,9 +21,9 @@ XML
it_behaves_like "a relayable Entity" it_behaves_like "a relayable Entity"
describe ".get_target_entity_type" do describe "#target_type" do
it "returns \"Poll\" as target type" do it "returns \"Poll\" as target type" do
expect(described_class.get_target_entity_type(data)).to eq("Poll") expect(described_class.new(data).target_type).to eq("Poll")
end end
end end
end end

View file

@ -12,9 +12,15 @@ module DiasporaFederation
class SomeRelayable < Entity class SomeRelayable < Entity
include Entities::Relayable include Entities::Relayable
property :diaspora_id, xml_name: :diaspora_handle
def target_type
"Target"
end
end end
describe ".verify_signatures" do describe "#verify_signatures" do
it "doesn't raise anything if correct data were passed" do it "doesn't raise anything if correct data were passed" do
hash[:author_signature] = Signing.sign_with_key(hash, author_pkey) hash[:author_signature] = Signing.sign_with_key(hash, author_pkey)
hash[:parent_author_signature] = Signing.sign_with_key(hash, parent_pkey) hash[:parent_author_signature] = Signing.sign_with_key(hash, parent_pkey)
@ -24,14 +30,14 @@ module DiasporaFederation
).and_return(author_pkey.public_key) ).and_return(author_pkey.public_key)
expect(DiasporaFederation.callbacks).to receive(:trigger).with( expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:fetch_author_public_key_by_entity_guid, "Post", hash[:parent_guid] :fetch_author_public_key_by_entity_guid, "Target", hash[:parent_guid]
).and_return(parent_pkey.public_key) ).and_return(parent_pkey.public_key)
expect(DiasporaFederation.callbacks).to receive(:trigger).with( expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:entity_author_is_local?, "Post", hash[:parent_guid] :entity_author_is_local?, "Target", hash[:parent_guid]
).and_return(false) ).and_return(false)
expect { Entities::Relayable.verify_signatures(hash, SomeRelayable) }.not_to raise_error expect { SomeRelayable.new(hash).verify_signatures }.not_to raise_error
end end
it "raises when no public key for author was fetched" do it "raises when no public key for author was fetched" do
@ -39,9 +45,9 @@ module DiasporaFederation
:fetch_public_key_by_diaspora_id, anything :fetch_public_key_by_diaspora_id, anything
).and_return(nil) ).and_return(nil)
expect { Entities::Relayable.verify_signatures(hash, SomeRelayable) }.to raise_error( expect {
Entities::Relayable::SignatureVerificationFailed SomeRelayable.new(hash).verify_signatures
) }.to raise_error Entities::Relayable::SignatureVerificationFailed
end end
it "raises when bad author signature was passed" do it "raises when bad author signature was passed" do
@ -51,9 +57,9 @@ module DiasporaFederation
:fetch_public_key_by_diaspora_id, hash[:diaspora_id] :fetch_public_key_by_diaspora_id, hash[:diaspora_id]
).and_return(author_pkey.public_key) ).and_return(author_pkey.public_key)
expect { Entities::Relayable.verify_signatures(hash, SomeRelayable) }.to raise_error( expect {
Entities::Relayable::SignatureVerificationFailed SomeRelayable.new(hash).verify_signatures
) }.to raise_error Entities::Relayable::SignatureVerificationFailed
end end
it "raises when no public key for parent author was fetched" do it "raises when no public key for parent author was fetched" do
@ -64,16 +70,16 @@ module DiasporaFederation
).and_return(author_pkey.public_key) ).and_return(author_pkey.public_key)
expect(DiasporaFederation.callbacks).to receive(:trigger).with( expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:fetch_author_public_key_by_entity_guid, "Post", hash[:parent_guid] :fetch_author_public_key_by_entity_guid, "Target", hash[:parent_guid]
).and_return(nil) ).and_return(nil)
expect(DiasporaFederation.callbacks).to receive(:trigger).with( expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:entity_author_is_local?, "Post", hash[:parent_guid] :entity_author_is_local?, "Target", hash[:parent_guid]
).and_return(false) ).and_return(false)
expect { Entities::Relayable.verify_signatures(hash, SomeRelayable) }.to raise_error( expect {
Entities::Relayable::SignatureVerificationFailed SomeRelayable.new(hash).verify_signatures
) }.to raise_error Entities::Relayable::SignatureVerificationFailed
end end
it "raises when bad parent author signature was passed" do it "raises when bad parent author signature was passed" do
@ -85,16 +91,16 @@ module DiasporaFederation
).and_return(author_pkey.public_key) ).and_return(author_pkey.public_key)
expect(DiasporaFederation.callbacks).to receive(:trigger).with( expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:fetch_author_public_key_by_entity_guid, "Post", hash[:parent_guid] :fetch_author_public_key_by_entity_guid, "Target", hash[:parent_guid]
).and_return(parent_pkey.public_key) ).and_return(parent_pkey.public_key)
expect(DiasporaFederation.callbacks).to receive(:trigger).with( expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:entity_author_is_local?, "Post", hash[:parent_guid] :entity_author_is_local?, "Target", hash[:parent_guid]
).and_return(false) ).and_return(false)
expect { Entities::Relayable.verify_signatures(hash, SomeRelayable) }.to raise_error( expect {
Entities::Relayable::SignatureVerificationFailed SomeRelayable.new(hash).verify_signatures
) }.to raise_error Entities::Relayable::SignatureVerificationFailed
end end
it "doesn't raise if parent_author_signature isn't set but we're on upstream federation" do it "doesn't raise if parent_author_signature isn't set but we're on upstream federation" do
@ -106,35 +112,33 @@ module DiasporaFederation
).and_return(author_pkey.public_key) ).and_return(author_pkey.public_key)
expect(DiasporaFederation.callbacks).to receive(:trigger).with( expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:entity_author_is_local?, "Post", hash[:parent_guid] :entity_author_is_local?, "Target", hash[:parent_guid]
).and_return(true) ).and_return(true)
expect { Entities::Relayable.verify_signatures(hash, SomeRelayable) }.not_to raise_error expect { SomeRelayable.new(hash).verify_signatures }.not_to raise_error
end end
end end
describe ".update_singatures!" do describe "#to_signed_h" do
it "updates signatures when they were nil and keys were supplied" do it "updates signatures when they were nil and keys were supplied" do
expect(DiasporaFederation.callbacks).to receive(:trigger).with( expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:fetch_private_key_by_diaspora_id, hash[:diaspora_id] :fetch_private_key_by_diaspora_id, hash[:diaspora_id]
).and_return(author_pkey) ).and_return(author_pkey)
expect(DiasporaFederation.callbacks).to receive(:trigger).with( expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:fetch_author_private_key_by_entity_guid, "Post", hash[:parent_guid] :fetch_author_private_key_by_entity_guid, "Target", hash[:parent_guid]
).and_return(parent_pkey) ).and_return(parent_pkey)
Entities::Relayable.update_signatures!(hash, SomeRelayable) signed_hash = SomeRelayable.new(hash).to_signed_h
expect(Signing.verify_signature(hash, hash[:author_signature], author_pkey)).to be_truthy
expect(Signing.verify_signature(hash, hash[:parent_author_signature], parent_pkey)).to be_truthy expect(Signing.verify_signature(signed_hash, signed_hash[:author_signature], author_pkey)).to be_truthy
expect(Signing.verify_signature(signed_hash, signed_hash[:parent_author_signature], parent_pkey)).to be_truthy
end end
it "doesn't change signatures if they are already set" do it "doesn't change signatures if they are already set" do
signatures = {author_signature: "aa", parent_author_signature: "bb"} hash.merge!(author_signature: "aa", parent_author_signature: "bb").delete(:some_other_data)
hash.merge!(signatures)
Entities::Relayable.update_signatures!(hash, SomeRelayable) expect(SomeRelayable.new(hash).to_signed_h).to eq(hash)
expect(hash[:author_signature]).to eq(signatures[:author_signature])
expect(hash[:parent_author_signature]).to eq(signatures[:parent_author_signature])
end end
it "doesn't change signatures if keys weren't supplied" do it "doesn't change signatures if keys weren't supplied" do
@ -143,12 +147,13 @@ module DiasporaFederation
).and_return(nil) ).and_return(nil)
expect(DiasporaFederation.callbacks).to receive(:trigger).with( expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:fetch_author_private_key_by_entity_guid, "Post", hash[:parent_guid] :fetch_author_private_key_by_entity_guid, "Target", hash[:parent_guid]
).and_return(nil) ).and_return(nil)
Entities::Relayable.update_signatures!(hash, SomeRelayable) signed_hash = SomeRelayable.new(hash).to_signed_h
expect(hash[:author_signature]).to eq(nil)
expect(hash[:parent_author_signature]).to eq(nil) expect(signed_hash[:author_signature]).to eq(nil)
expect(signed_hash[:parent_author_signature]).to eq(nil)
end end
end end
end end

View file

@ -147,7 +147,7 @@ XML
it "calls signatures verification on relayable unpack" do it "calls signatures verification on relayable unpack" do
entity = FactoryGirl.build(:comment_entity) entity = FactoryGirl.build(:comment_entity)
payload = Salmon::XmlPayload.pack(entity) payload = Salmon::XmlPayload.pack(entity)
expect(Entities::Relayable).to receive(:verify_signatures).once expect(Signing).to receive(:verify_signature).twice.and_call_original
Salmon::XmlPayload.unpack(payload) Salmon::XmlPayload.unpack(payload)
end end
end end