move signing logic for relayables to Relayable

refactoring exceptions for relayables
This commit is contained in:
Benjamin Neff 2016-01-31 15:56:01 +01:00
parent 714f6d8273
commit b19e1b8e52
11 changed files with 152 additions and 194 deletions

View file

@ -7,7 +7,6 @@ require "diaspora_federation/validators"
require "diaspora_federation/fetcher"
require "diaspora_federation/signing"
require "diaspora_federation/entities"
require "diaspora_federation/discovery"
@ -221,7 +220,7 @@ module DiasporaFederation
end
def configuration_error(message)
logger.fatal("diaspora federation configuration error: #{message}")
logger.fatal "diaspora federation configuration error: #{message}"
raise ConfigurationError, message
end
end

View file

@ -2,9 +2,14 @@ module DiasporaFederation
module Entities
# this is a module that defines common properties for relayable entities
# which include Like, Comment, Participation, Message, etc. Each relayable
# has a parent, identified by guid. Relayables also are signed and signing/verificating
# has a parent, identified by guid. Relayables also are 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
# on inclusion of this module the required properties for a relayable are added to the object that includes it
#
# @!attribute [r] parent_guid
@ -43,15 +48,12 @@ module DiasporaFederation
super.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?
raise AuthorPrivateKeyNotFound, "author=#{diaspora_id} guid=#{guid}" if privkey.nil?
hash[:author_signature] = sign_with_key(privkey, hash)
logger.info "event=sign_with_key signature=author_signature author=#{diaspora_id} guid=#{guid}"
end
if parent_author_signature.nil?
privkey = DiasporaFederation.callbacks.trigger(
:fetch_author_private_key_by_entity_guid, parent_type, parent_guid
)
hash[:parent_author_signature] = Signing.sign_with_key(hash, privkey) unless privkey.nil?
end
try_sign_with_parent_author(hash) if parent_author_signature.nil?
end
end
@ -66,33 +68,84 @@ module DiasporaFederation
end
end
# Exception raised when verify_signatures fails to verify signatures (signatures are wrong)
class SignatureVerificationFailed < ArgumentError
end
# verifies the signatures (+author_signature+ and +parent_author_signature+ if needed)
# @raise [SignatureVerificationFailed] if the signature is not valid or no public key is found
def verify_signatures
pubkey = DiasporaFederation.callbacks.trigger(:fetch_public_key_by_diaspora_id, diaspora_id)
raise SignatureVerificationFailed, "failed to fetch public key for #{diaspora_id}" if pubkey.nil?
raise SignatureVerificationFailed, "wrong author_signature" unless Signing.verify_signature(
data, author_signature, pubkey
)
raise PublicKeyNotFound, "author_signature author=#{diaspora_id} guid=#{guid}" if pubkey.nil?
raise SignatureVerificationFailed, "wrong author_signature" unless verify_signature(pubkey, author_signature)
author_is_local = DiasporaFederation.callbacks.trigger(:entity_author_is_local?, parent_type, parent_guid)
verify_parent_signature unless author_is_local
parent_author_local = DiasporaFederation.callbacks.trigger(:entity_author_is_local?, parent_type, parent_guid)
verify_parent_author_signature unless parent_author_local
end
private
# sign with parent author, if the parent author is local (if the private key is found)
# @param [Hash] hash the hash to sign
def try_sign_with_parent_author(hash)
privkey = DiasporaFederation.callbacks.trigger(
:fetch_author_private_key_by_entity_guid, parent_type, parent_guid
)
unless privkey.nil?
hash[:parent_author_signature] = sign_with_key(privkey, hash)
logger.info "event=sign_with_key signature=parent_author_signature guid=#{guid}"
end
end
# this happens only on downstream federation
def verify_parent_signature
def verify_parent_author_signature
pubkey = DiasporaFederation.callbacks.trigger(:fetch_author_public_key_by_entity_guid, parent_type, parent_guid)
raise SignatureVerificationFailed, "failed to fetch public key for author of #{parent_guid}" if pubkey.nil?
raise SignatureVerificationFailed, "wrong parent_author_signature" unless Signing.verify_signature(
data, parent_author_signature, pubkey
)
raise PublicKeyNotFound, "parent_author_signature parent_guid=#{parent_guid} guid=#{guid}" if pubkey.nil?
unless verify_signature(pubkey, parent_author_signature)
raise SignatureVerificationFailed, "wrong parent_author_signature parent_guid=#{parent_guid}"
end
end
# Sign the data with the key
#
# @param [OpenSSL::PKey::RSA] privkey An RSA key
# @param [Hash] hash data to sign
# @return [String] A Base64 encoded signature of #signable_string with key
def sign_with_key(privkey, hash)
Base64.strict_encode64(privkey.sign(DIGEST, signable_string(hash)))
end
# Check that signature is a correct signature
#
# @param [OpenSSL::PKey::RSA] pubkey An RSA key
# @param [String] signature The signature to be verified.
# @return [Boolean]
def verify_signature(pubkey, signature)
if signature.nil?
logger.warn "event=verify_signature status=abort reason=no_signature guid=#{guid}"
return false
end
validity = pubkey.verify(DIGEST, Base64.decode64(signature), signable_string(data))
logger.info "event=verify_signature status=complete guid=#{guid} validity=#{validity}"
validity
end
# @param [Hash] hash data to sign
# @return [String] signature data string
def signable_string(hash)
hash.map {|name, value|
value.to_s unless name =~ /signature/
}.compact.join(";")
end
# Exception raised when creating the author_signature failes, because the private key was not found
class AuthorPrivateKeyNotFound < RuntimeError
end
# Exception raised when verify_signatures fails to verify signatures (no public key found)
class PublicKeyNotFound < RuntimeError
end
# Exception raised when verify_signatures fails to verify signatures (signatures are wrong)
class SignatureVerificationFailed < RuntimeError
end
end
end

View file

@ -64,7 +64,7 @@ module DiasporaFederation
# @param [SignedRetraction, RelayableRetraction] ret the retraction to sign
# @return [String] a Base64 encoded signature of the retraction with the key
def self.sign_with_key(privkey, ret)
Base64.strict_encode64(privkey.sign(OpenSSL::Digest::SHA256.new, [ret.target_guid, ret.target_type].join(";")))
Base64.strict_encode64(privkey.sign(Relayable::DIGEST, [ret.target_guid, ret.target_type].join(";")))
end
end
end

View file

@ -1,11 +1,11 @@
module DiasporaFederation
module Federation
# Raised if failed to fetch a public key of the sender of the received message
class SenderKeyNotFound < Exception
class SenderKeyNotFound < RuntimeError
end
# Raised if recipient private key is missing for a private receive
class RecipientKeyNotFound < Exception
class RecipientKeyNotFound < RuntimeError
end
end
end

View file

@ -1,55 +0,0 @@
module DiasporaFederation
# this module defines operations of signing an arbitrary hash with an arbitrary key
module Signing
extend Logging
# Sign the data with the key
#
# @param [Hash] hash data to sign
# @param [OpenSSL::PKey::RSA] privkey An RSA key
# @return [String] A Base64 encoded signature of #signable_string with key
def self.sign_with_key(hash, privkey)
sig = Base64.strict_encode64(
privkey.sign(
OpenSSL::Digest::SHA256.new,
signable_string(hash)
)
)
logger.info "event=sign_with_key status=complete guid=#{hash[:guid]}"
sig
end
# Check that signature is a correct signature
#
# @param [Hash] hash data to verify
# @param [String] signature The signature to be verified.
# @param [OpenSSL::PKey::RSA] pubkey An RSA key
# @return [Boolean]
def self.verify_signature(hash, signature, pubkey)
if pubkey.nil?
logger.warn "event=verify_signature status=abort reason=no_key guid=#{hash[:guid]}"
return false
elsif signature.nil?
logger.warn "event=verify_signature status=abort reason=no_signature guid=#{hash[:guid]}"
return false
end
validity = pubkey.verify(
OpenSSL::Digest::SHA256.new,
Base64.decode64(signature),
signable_string(hash)
)
logger.info "event=verify_signature status=complete guid=#{hash[:guid]} validity=#{validity}"
validity
end
# @param [Hash] hash data to sign
# @return [String] signature data string
def self.signable_string(hash)
hash.map {|name, value|
value.to_s unless name =~ /signature/
}.compact.join(";")
end
private_class_method :signable_string
end
end

View file

@ -31,12 +31,12 @@ XML
:fetch_private_key_by_diaspora_id, hash[:diaspora_id]
).and_return(author_pkey)
signed_string = "#{hash[:target_guid]};#{hash[:target_type]}"
signed_hash = Entities::RelayableRetraction.new(hash).to_h
signable_hash = hash.select do |key, _|
%i(target_guid target_type).include?(key)
end
expect(Signing.verify_signature(signable_hash, signed_hash[:target_author_signature], author_pkey)).to be_truthy
signature = Base64.decode64(signed_hash[:target_author_signature])
expect(author_pkey.verify(OpenSSL::Digest::SHA256.new, signature, signed_string)).to be_truthy
end
it "updates parent author signature when it was nil, key was supplied and sender is not author of the target" do
@ -48,12 +48,12 @@ XML
:fetch_private_key_by_diaspora_id, hash[:diaspora_id]
).and_return(author_pkey)
signed_string = "#{hash[:target_guid]};#{hash[:target_type]}"
signed_hash = Entities::RelayableRetraction.new(hash).to_h
signable_hash = hash.select do |key, _|
%i(target_guid target_type).include?(key)
end
expect(Signing.verify_signature(signable_hash, signed_hash[:parent_author_signature], author_pkey)).to be_truthy
signature = Base64.decode64(signed_hash[:parent_author_signature])
expect(author_pkey.verify(OpenSSL::Digest::SHA256.new, signature, signed_string)).to be_truthy
end
it "doesn't change signatures if they are already set" do

View file

@ -4,6 +4,7 @@ module DiasporaFederation
let(:parent_pkey) { OpenSSL::PKey::RSA.generate(1024) }
let(:hash) {
{
guid: FactoryGirl.generate(:guid),
diaspora_id: FactoryGirl.generate(:diaspora_id),
parent_guid: FactoryGirl.generate(:guid),
some_other_data: "a_random_string"
@ -11,33 +12,39 @@ module DiasporaFederation
}
class SomeRelayable < Entity
include Entities::Relayable
property :guid
property :diaspora_id, xml_name: :diaspora_handle
include Entities::Relayable
def parent_type
"Target"
"Parent"
end
end
def legacy_sign_with_key(privkey, hash)
Base64.strict_encode64(privkey.sign(OpenSSL::Digest::SHA256.new, hash.values.join(";")))
end
describe "#verify_signatures" do
it "doesn't raise anything if correct data were passed" do
hash[:author_signature] = Signing.sign_with_key(hash, author_pkey)
hash[:parent_author_signature] = Signing.sign_with_key(hash, parent_pkey)
signed_hash = hash.dup
signed_hash[:author_signature] = legacy_sign_with_key(author_pkey, hash)
signed_hash[:parent_author_signature] = legacy_sign_with_key(parent_pkey, hash)
expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:fetch_public_key_by_diaspora_id, hash[:diaspora_id]
).and_return(author_pkey.public_key)
expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:fetch_author_public_key_by_entity_guid, "Target", hash[:parent_guid]
:fetch_author_public_key_by_entity_guid, "Parent", hash[:parent_guid]
).and_return(parent_pkey.public_key)
expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:entity_author_is_local?, "Target", hash[:parent_guid]
:entity_author_is_local?, "Parent", hash[:parent_guid]
).and_return(false)
expect { SomeRelayable.new(hash).verify_signatures }.not_to raise_error
expect { SomeRelayable.new(signed_hash).verify_signatures }.not_to raise_error
end
it "raises when no public key for author was fetched" do
@ -47,7 +54,7 @@ module DiasporaFederation
expect {
SomeRelayable.new(hash).verify_signatures
}.to raise_error Entities::Relayable::SignatureVerificationFailed
}.to raise_error Entities::Relayable::PublicKeyNotFound
end
it "raises when bad author signature was passed" do
@ -63,27 +70,27 @@ module DiasporaFederation
end
it "raises when no public key for parent author was fetched" do
hash[:author_signature] = Signing.sign_with_key(hash, author_pkey)
hash[:author_signature] = legacy_sign_with_key(author_pkey, hash)
expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:fetch_public_key_by_diaspora_id, hash[:diaspora_id]
).and_return(author_pkey.public_key)
expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:fetch_author_public_key_by_entity_guid, "Target", hash[:parent_guid]
:fetch_author_public_key_by_entity_guid, "Parent", hash[:parent_guid]
).and_return(nil)
expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:entity_author_is_local?, "Target", hash[:parent_guid]
:entity_author_is_local?, "Parent", hash[:parent_guid]
).and_return(false)
expect {
SomeRelayable.new(hash).verify_signatures
}.to raise_error Entities::Relayable::SignatureVerificationFailed
}.to raise_error Entities::Relayable::PublicKeyNotFound
end
it "raises when bad parent author signature was passed" do
hash[:author_signature] = Signing.sign_with_key(hash, author_pkey)
hash[:author_signature] = legacy_sign_with_key(author_pkey, hash)
hash[:parent_author_signature] = nil
expect(DiasporaFederation.callbacks).to receive(:trigger).with(
@ -91,11 +98,11 @@ module DiasporaFederation
).and_return(author_pkey.public_key)
expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:fetch_author_public_key_by_entity_guid, "Target", hash[:parent_guid]
:fetch_author_public_key_by_entity_guid, "Parent", hash[:parent_guid]
).and_return(parent_pkey.public_key)
expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:entity_author_is_local?, "Target", hash[:parent_guid]
:entity_author_is_local?, "Parent", hash[:parent_guid]
).and_return(false)
expect {
@ -104,7 +111,7 @@ module DiasporaFederation
end
it "doesn't raise if parent_author_signature isn't set but we're on upstream federation" do
hash[:author_signature] = Signing.sign_with_key(hash, author_pkey)
hash[:author_signature] = legacy_sign_with_key(author_pkey, hash)
hash[:parent_author_signature] = nil
expect(DiasporaFederation.callbacks).to receive(:trigger).with(
@ -112,7 +119,7 @@ module DiasporaFederation
).and_return(author_pkey.public_key)
expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:entity_author_is_local?, "Target", hash[:parent_guid]
:entity_author_is_local?, "Parent", hash[:parent_guid]
).and_return(true)
expect { SomeRelayable.new(hash).verify_signatures }.not_to raise_error
@ -126,13 +133,19 @@ module DiasporaFederation
).and_return(author_pkey)
expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:fetch_author_private_key_by_entity_guid, "Target", hash[:parent_guid]
:fetch_author_private_key_by_entity_guid, "Parent", hash[:parent_guid]
).and_return(parent_pkey)
signed_string = hash.reject {|key, _| key == :some_other_data }.values.join(";")
signed_hash = SomeRelayable.new(hash).to_h
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
def verify_signature(pubkey, signature, signed_string)
pubkey.verify(OpenSSL::Digest::SHA256.new, Base64.decode64(signature), signed_string)
end
expect(verify_signature(author_pkey, signed_hash[:author_signature], signed_string)).to be_truthy
expect(verify_signature(parent_pkey, signed_hash[:parent_author_signature], signed_string)).to be_truthy
end
it "doesn't change signatures if they are already set" do
@ -141,18 +154,27 @@ module DiasporaFederation
expect(SomeRelayable.new(hash).to_h).to eq(hash)
end
it "doesn't change signatures if keys weren't supplied" do
it "raises when author_signature not set and key isn't supplied" do
expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:fetch_private_key_by_diaspora_id, hash[:diaspora_id]
).and_return(nil)
expect {
SomeRelayable.new(hash).to_h
}.to raise_error Entities::Relayable::AuthorPrivateKeyNotFound
end
it "doesn't set parent_author_signature if key isn't supplied" do
expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:fetch_author_private_key_by_entity_guid, "Target", hash[:parent_guid]
:fetch_private_key_by_diaspora_id, hash[:diaspora_id]
).and_return(author_pkey)
expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:fetch_author_private_key_by_entity_guid, "Parent", hash[:parent_guid]
).and_return(nil)
signed_hash = SomeRelayable.new(hash).to_h
expect(signed_hash[:author_signature]).to eq(nil)
expect(signed_hash[:parent_author_signature]).to eq(nil)
end
end

View file

@ -26,13 +26,14 @@ XML
:fetch_private_key_by_diaspora_id, hash[:diaspora_id]
).and_return(author_pkey)
signable_hash = hash.select do |key, _|
%i(target_guid target_type).include?(key)
end
signed_string = "#{hash[:target_guid]};#{hash[:target_type]}"
signed_hash = Entities::SignedRetraction.new(hash).to_h
expect(Signing.verify_signature(signable_hash, signed_hash[:target_author_signature], author_pkey)).to be_truthy
valid = author_pkey.verify(
OpenSSL::Digest::SHA256.new, Base64.decode64(signed_hash[:target_author_signature]), signed_string
)
expect(valid).to be_truthy
end
it "doesn't change signature if it is already set" do

View file

@ -147,8 +147,11 @@ XML
it "calls signatures verification on relayable unpack" do
entity = FactoryGirl.build(:comment_entity)
payload = Salmon::XmlPayload.pack(entity)
expect(Signing).to receive(:verify_signature).twice.and_call_original
Salmon::XmlPayload.unpack(payload)
payload.at_xpath("post/*[1]/author_signature").content = nil
expect {
Salmon::XmlPayload.unpack(payload)
}.to raise_error DiasporaFederation::Entities::Relayable::SignatureVerificationFailed
end
end

View file

@ -1,68 +0,0 @@
module DiasporaFederation
describe Signing do
let(:privkey) {
OpenSSL::PKey::RSA.new <<-RSA
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDT7vBTAl0Z55bPcBjM9dvSOTuVtBxsgfrw2W0hTAYpd1H5032C
cVW3mqd0l/9BHscgudVFAkvp+nf+wTQILn4qH4YAhOdWgrlSBA6Rbs3cmtmXzGNq
oQr4NOMbqs6sP+bBjDuDdB+cAFms/NDUH3cHBKPXi3e3csxiErmN1zyfWwIDAQAB
AoGAbpBC1CtxgqgtJz8l0ReafIvbJ/h0s68DyU7E/g/5TvyuyZSp77lMrKKEJfF9
+u0hmVMZjgzqqcA/haopiPMoYcJAwwhJLeXAgAWA+8j60Y524WLDcMPwMxQvVFd9
3FYXdOalojDoS34BWeBy6Gt+lLGyDvo/NnJBqIMPN0/KzYECQQDuslE4f1+RHhUq
wf2rL/7gCgrnkDOcH1SPjN2FrKG5ALmjThCq7Wr1Umj81uvmglfpIRY/ORgYgujA
kwNTB1ohAkEA40v0mHaYDegL//jucFmx/iK9Bs/722rJGIXI7bGIwLRC1hW101h3
DLMEMT0QaamVEEnrXFdqhjz+bfYfqUkh+wJAU3a+t8ayIAgo1p6mmKlbsfNRBM+D
fF/oLZnQC+HlWs9KGjQ918bU05tRYre0HRIOs1ICeXD5X/jGci/1xZ6YgQJAJony
Zwd0sKbvoe8rPpF2xIhPVKBfK8znW+kTMHoxnbryuinkMnmFdfnEdDTOW5wNUj22
Umnf/fLJkQtyQtnLkQJBANMoQPrP6aMRh45bhq+y6DbzHHHc2T5cuGBCtnhu+qrK
hWHXqQT4rArfq8YBpvDUa7qD13WwFGK3TPRpQSVGzNg=
-----END RSA PRIVATE KEY-----
RSA
}
let(:hash) {
{
param1: "1",
param2: "2",
signature: "SIGNATURE_VALUE==",
param3: "3",
parent_signature: "SIGNATURE2_VALUE==",
param4: "4"
}
}
let(:signature) {
"OesXlpesuLcA0t8gPyBjvznvkl0pz63p8z6+o2fxFNUaZkuR6YQv/sJOTSMPYBAFwcWr048Ol7yw4jSHq0gFCdBBeF7Mg287jktCie"\
"xa6G6mA24hBlOWnyRJLV2OyqcTU1P5pXWlUc1Mbwbr6bSIs6VK9djFMLLQ6wjjpusJ0XU="
}
describe ".signable_string" do
it "forms correct string for a hash" do
expect(Signing.send(:signable_string, hash)).to eq("1;2;3;4")
end
end
describe ".sign_with_key" do
it "produces correct signature" do
expect(Signing.sign_with_key(hash, privkey)).to eq(signature)
end
end
describe ".verify_signature" do
it "verifies correct signature" do
expect(Signing.verify_signature(hash, signature, privkey.public_key)).to be_truthy
end
it "doesn't verify wrong signature" do
expect(Signing.verify_signature(hash, "false signature==", privkey.public_key)).to be_falsy
end
it "doesn't verify when signature is missing" do
expect(Signing.verify_signature(hash, nil, privkey.public_key)).to be_falsy
end
it "doesn't verify when public key is missing" do
expect(Signing.verify_signature(hash, signature, nil)).to be_falsy
end
end
end
end

View file

@ -75,17 +75,20 @@ shared_examples "a relayable Entity" do
let(:instance) { described_class.new(data.merge(author_signature: nil, parent_author_signature: nil)) }
context "signatures generation" do
def legacy_verify_signature(pubkey, signature, signed_string)
pubkey.verify(OpenSSL::Digest::SHA256.new, Base64.decode64(signature), signed_string)
end
it "computes correct signatures for the entity" do
hash = instance.to_h
signed_string = instance.to_h.map {|name, value| value.to_s unless name =~ /signature/ }.compact.join(";")
xml = DiasporaFederation::Salmon::XmlPayload.pack(instance)
author_signature = xml.at_xpath("post/*[1]/author_signature").text
parent_author_signature = xml.at_xpath("post/*[1]/parent_author_signature").text
expect(DiasporaFederation::Signing.verify_signature(hash, author_signature, test_pkey))
.to be_truthy
expect(DiasporaFederation::Signing.verify_signature(hash, parent_author_signature, test_pkey))
.to be_truthy
expect(legacy_verify_signature(test_pkey, author_signature, signed_string)).to be_truthy
expect(legacy_verify_signature(test_pkey, parent_author_signature, signed_string)).to be_truthy
end
end
end