Merge pull request #89 from cmrd-senya/account_migration_changes

Extract singning of AccountMigration to a different module and introduce alternative form for account migration message
This commit is contained in:
Benjamin Neff 2017-12-27 20:03:17 +01:00
commit d9a02119b8
No known key found for this signature in database
GPG key ID: 971464C3F1A90194
11 changed files with 360 additions and 150 deletions

View file

@ -8,9 +8,16 @@ This entity is sent when a person changes their diaspora* ID (e.g. when a user m
| Property | Type | Description |
| ----------- | ---------------------------- | ------------------------------------------------------------------------------------ |
| `author` | [diaspora\* ID][diaspora-id] | The diaspora\* ID of the closed account. |
| `person` | [Profile][profile] | New profile of a person |
| `signature` | [Signature][signature] | Signature that validates original and target diaspora* IDs with the new key of person |
| `author` | [diaspora\* ID][diaspora-id] | The diaspora\* ID of the sender of the entity. The entity may be sent by either old user identity or new user identity. |
| `person` | [Profile][profile] | New profile of a person. |
| `signature` | [Signature][signature] | Signature that validates original and target diaspora* IDs with the private key of the second identity, other than the entity author. So if the author is the old identity then this signature is made with the new identity key, and vice versa. |
## Optional Properties
| Property | Type | Description |
| ----------- | ---------------------------- | ------------------------------------------------------------------------------------ |
| `old_identity` | [diaspora\* ID][diaspora-id] | The diaspora\* ID of the closed account. This field is mandatory if the author of the entity is the new identity. |
### Signature
@ -51,6 +58,7 @@ AccountMigration:old-diaspora-id@example.org:new-diaspora-id@example.com
<signature>
07b1OIY6sTUQwV5pbpgFK0uz6W4cu+oQnlg410Q4uISUOdNOlBdYqhZJm62VFhgvzt4TZXfiJgoupFkRjP0BsaVaZuP2zKMNvO3ngWOeJRf2oRK4Ub5cEA/g7yijkRc+7y8r1iLJ31MFb1czyeCsLxw9Ol8SvAJddogGiLHDhjE=
</signature>
<old_identity>alice@example.org</old_identity>
</account_migration>
~~~

View file

@ -13,6 +13,7 @@ require "diaspora_federation/entities/related_entity"
# abstract types
require "diaspora_federation/entities/post"
require "diaspora_federation/entities/signable"
require "diaspora_federation/entities/account_migration/signable"
require "diaspora_federation/entities/relayable"
# types

View file

@ -5,10 +5,12 @@ module DiasporaFederation
#
# @see Validators::AccountMigrationValidator
class AccountMigration < Entity
include Signable
include AccountMigration::Signable
# @!attribute [r] author
# The old diaspora* ID of the person who changes their ID
# Sender of the AccountMigration message. Usually it is the old diaspora* ID of the person who changes their ID.
# This property is also allowed to be the new diaspora* ID, which is equal to the author of the included
# profile.
# @see Person#author
# @return [String] author diaspora* ID
property :author, :string
@ -19,20 +21,39 @@ module DiasporaFederation
entity :profile, Entities::Profile
# @!attribute [r] signature
# Signature that validates original and target diaspora* IDs with the new key of person
# Signature that validates original and target diaspora* IDs with the private key of the second identity, other
# than the entity author. So if the author is the old identity then this signature is made with the new identity
# key, and vice versa.
# @return [String] signature
property :signature, :string, default: nil
# @return [String] string representation of this object
def to_s
"AccountMigration:#{author}:#{profile.author}"
# @!attribute [r] old_identity
# Optional attribute which keeps old diaspora* ID. Must be present when author attribute contains new diaspora*
# ID.
# @return [String] old identity
property :old_identity, :string, default: nil
# Returns diaspora* ID of the old person identity.
# @return [String] diaspora* ID of the old person identity
def old_identity
return @old_identity if author_is_new_id?
author
end
# Returns diaspora* ID of the new person identity.
# @return [String] diaspora* ID of the new person identity
def new_identity
profile.author
end
# @return [String] string representation of this object
alias to_s unique_migration_descriptor
# Shortcut for calling super method with sensible arguments
#
# @see DiasporaFederation::Entities::Signable#verify_signature
def verify_signature
super(profile.author, :signature)
super(signer_id, :signature)
end
# Calls super and additionally does signature verification for the instantiated entity.
@ -44,30 +65,33 @@ module DiasporaFederation
private
# @see DiasporaFederation::Entities::Signable#signature_data
def signature_data
to_s
def author_is_new_id?
author == new_identity
end
def signer_id
author_is_new_id? ? @old_identity : new_identity
end
def enriched_properties
super.tap do |hash|
hash[:signature] = signature || sign_with_new_key
hash[:signature] = signature || sign_with_respective_key
end
end
# Sign with new user's key
# @raise [NewPrivateKeyNotFound] if the new user's private key is not found
# Sign with the key of the #signer_id identity
# @raise [PrivateKeyNotFound] if the signer's private key is not found
# @return [String] A Base64 encoded signature of #signature_data with key
def sign_with_new_key
privkey = DiasporaFederation.callbacks.trigger(:fetch_private_key, profile.author)
raise NewPrivateKeyNotFound, "author=#{profile.author} obj=#{self}" if privkey.nil?
def sign_with_respective_key
privkey = DiasporaFederation.callbacks.trigger(:fetch_private_key, signer_id)
raise PrivateKeyNotFound, "signer=#{signer_id} obj=#{self}" if privkey.nil?
sign_with_key(privkey).tap do
logger.info "event=sign status=complete signature=signature author=#{profile.author} obj=#{self}"
logger.info "event=sign status=complete signature=signature signer=#{signer_id} obj=#{self}"
end
end
# Raised, if creating the signature fails, because the new private key of a user was not found
class NewPrivateKeyNotFound < RuntimeError
class PrivateKeyNotFound < RuntimeError
end
end
end

View file

@ -0,0 +1,24 @@
module DiasporaFederation
module Entities
class AccountMigration < Entity
# AccountMigration::Signable is a module that encapsulates basic signature generation/verification flow for
# AccountMigration entity.
#
# It is possible that implementation of diaspora* protocol requires to compute the signature for the
# AccountMigration entity without instantiating the entity. In this case this module may be useful.
module Signable
include Entities::Signable
# @return [String] string which is uniquely represents migration occasion
def unique_migration_descriptor
"AccountMigration:#{old_identity}:#{new_identity}"
end
# @see DiasporaFederation::Entities::Signable#signature_data
def signature_data
unique_migration_descriptor
end
end
end
end
end

View file

@ -42,6 +42,7 @@ module DiasporaFederation
Fabricator(:account_migration_entity, class_name: DiasporaFederation::Entities::AccountMigration) do
author { Fabricate.sequence(:diaspora_id) }
profile { Fabricate(:profile_entity) }
old_identity { Fabricate.sequence(:diaspora_id) }
end
Fabricator(:person_entity, class_name: DiasporaFederation::Entities::Person) do

View file

@ -7,6 +7,8 @@ module DiasporaFederation
rule :author, :diaspora_id
rule :profile, :not_nil
rule :old_identity, :diaspora_id
end
end
end

View file

@ -0,0 +1,39 @@
module DiasporaFederation
describe Entities::AccountMigration::Signable do
let(:entity) { TestAMSignableEntity.new({}) }
class TestAMSignableEntity < Entity
include Entities::AccountMigration::Signable
property :my_signature, :string, default: nil
def old_identity
"old"
end
def new_identity
"new"
end
def freeze; end
end
it_behaves_like "a signable" do
let(:test_class) { TestAMSignableEntity }
let(:test_string) { "AccountMigration:old:new" }
end
describe "#unique_migration_descriptor" do
it "composes a string using #old_identity and #new_identity" do
expect(entity.unique_migration_descriptor).to eq("AccountMigration:old:new")
end
end
describe "#signature_data" do
it "delegates to #unique_migration_descriptor" do
expect(entity).to receive(:unique_migration_descriptor).and_return("test123")
expect(entity.signature_data).to eq("test123")
end
end
end
end

View file

@ -1,21 +1,185 @@
module DiasporaFederation
describe Entities::AccountMigration do
let(:new_diaspora_id) { alice.diaspora_id }
let(:new_author_pkey) { alice.private_key }
let(:hash) {
Fabricate.attributes_for(:account_deletion_entity).merge(
profile: Fabricate(:profile_entity, author: new_diaspora_id)
)
}
let(:old_user) { Fabricate(:user) }
let(:new_user) { Fabricate(:user) }
let(:old_diaspora_id) { old_user.diaspora_id }
let(:new_diaspora_id) { new_user.diaspora_id }
let(:data) {
hash.tap {|hash|
properties = described_class.new(hash).send(:enriched_properties)
hash[:signature] = properties[:signature]
}
}
let(:signature_data) { "AccountMigration:#{hash[:author]}:#{new_diaspora_id}" }
let(:signature_data) { "AccountMigration:#{old_diaspora_id}:#{new_diaspora_id}" }
let(:string) { signature_data }
let(:xml) { <<-XML }
shared_examples_for "an account migration entity" do
it_behaves_like "an Entity subclass"
it_behaves_like "an XML Entity"
describe "#to_xml" do
it "computes signature when no signature was provided" do
expect_callback(:fetch_private_key, signer_id).and_return(signer_pkey)
entity = Entities::AccountMigration.new(hash)
xml = entity.to_xml
signature = xml.at_xpath("signature").text
expect(verify_signature(signer_pkey, signature, entity.to_s)).to be_truthy
end
it "doesn't change signature if it is already set" do
hash[:signature] = "aa"
xml = Entities::AccountMigration.new(hash).to_xml
expect(xml.at_xpath("signature").text).to eq("aa")
end
it "raises when signature isn't set and key isn't supplied" do
expect_callback(:fetch_private_key, signer_id).and_return(nil)
expect {
Entities::AccountMigration.new(hash).to_xml
}.to raise_error Entities::AccountMigration::PrivateKeyNotFound
end
end
describe "#verify_signature" do
it "doesn't raise anything if correct signature was passed" do
hash[:signature] = sign_with_key(signer_pkey, signature_data)
expect_callback(:fetch_public_key, signer_id).and_return(signer_pkey)
expect { Entities::AccountMigration.new(hash).verify_signature }.not_to raise_error
end
it "raises when no public key for author was fetched" do
expect_callback(:fetch_public_key, anything).and_return(nil)
expect {
Entities::AccountMigration.new(hash).verify_signature
}.to raise_error Entities::AccountMigration::PublicKeyNotFound
end
it "raises when bad author signature was passed" do
hash[:signature] = "abcdef"
expect_callback(:fetch_public_key, signer_id).and_return(signer_pkey.public_key)
expect {
Entities::AccountMigration.new(hash).verify_signature
}.to raise_error Entities::AccountMigration::SignatureVerificationFailed
end
end
describe ".from_hash" do
it "calls #verify_signature" do
expect_any_instance_of(Entities::AccountMigration).to receive(:freeze)
expect_any_instance_of(Entities::AccountMigration).to receive(:verify_signature)
Entities::AccountMigration.from_hash(hash)
end
it "raises when bad author signature was passed" do
hash[:signature] = "abcdef"
expect_callback(:fetch_public_key, signer_id).and_return(signer_pkey.public_key)
expect {
Entities::AccountMigration.from_hash(hash)
}.to raise_error Entities::AccountMigration::SignatureVerificationFailed
end
end
end
context "with old identity as author" do
let(:signer_id) { new_diaspora_id }
let(:signer_pkey) { new_user.private_key }
let(:hash) {
{
author: old_diaspora_id,
profile: Fabricate(:profile_entity, author: new_diaspora_id),
old_identity: old_diaspora_id
}
}
let(:xml) { <<-XML }
<account_migration>
<author>#{data[:author]}</author>
<profile>
<author>#{data[:profile].author}</author>
<first_name>#{data[:profile].first_name}</first_name>
<image_url>#{data[:profile].image_url}</image_url>
<image_url_medium>#{data[:profile].image_url}</image_url_medium>
<image_url_small>#{data[:profile].image_url}</image_url_small>
<bio>#{data[:profile].bio}</bio>
<birthday>#{data[:profile].birthday}</birthday>
<gender>#{data[:profile].gender}</gender>
<location>#{data[:profile].location}</location>
<searchable>#{data[:profile].searchable}</searchable>
<public>#{data[:profile].public}</public>
<nsfw>#{data[:profile].nsfw}</nsfw>
<tag_string>#{data[:profile].tag_string}</tag_string>
</profile>
<signature>#{data[:signature]}</signature>
<old_identity>#{data[:old_identity]}</old_identity>
</account_migration>
XML
it_behaves_like "an account migration entity"
end
context "with new identity as author" do
let(:signer_id) { old_diaspora_id }
let(:signer_pkey) { old_user.private_key }
let(:hash) {
{
author: new_diaspora_id,
profile: Fabricate(:profile_entity, author: new_diaspora_id),
old_identity: old_diaspora_id
}
}
let(:xml) { <<-XML }
<account_migration>
<author>#{data[:author]}</author>
<profile>
<author>#{data[:profile].author}</author>
<first_name>#{data[:profile].first_name}</first_name>
<image_url>#{data[:profile].image_url}</image_url>
<image_url_medium>#{data[:profile].image_url}</image_url_medium>
<image_url_small>#{data[:profile].image_url}</image_url_small>
<bio>#{data[:profile].bio}</bio>
<birthday>#{data[:profile].birthday}</birthday>
<gender>#{data[:profile].gender}</gender>
<location>#{data[:profile].location}</location>
<searchable>#{data[:profile].searchable}</searchable>
<public>#{data[:profile].public}</public>
<nsfw>#{data[:profile].nsfw}</nsfw>
<tag_string>#{data[:profile].tag_string}</tag_string>
</profile>
<signature>#{data[:signature]}</signature>
<old_identity>#{data[:old_identity]}</old_identity>
</account_migration>
XML
it_behaves_like "an account migration entity"
end
context "when author is the new identity and old_identity prop is missing" do
let(:signer_id) { old_diaspora_id }
let(:signer_pkey) { old_user.private_key }
let(:hash) {
{
author: new_diaspora_id,
profile: Fabricate(:profile_entity, author: new_diaspora_id)
}
}
let(:xml) { <<-XML }
<account_migration>
<author>#{data[:author]}</author>
<profile>
@ -37,81 +201,16 @@ module DiasporaFederation
</account_migration>
XML
let(:string) { "AccountMigration:#{data[:author]}:#{data[:profile].author}" }
it_behaves_like "an Entity subclass"
it_behaves_like "an XML Entity"
describe "#to_xml" do
it "computes signature when no signature was provided" do
expect_callback(:fetch_private_key, new_diaspora_id).and_return(new_author_pkey)
entity = Entities::AccountMigration.new(hash)
xml = entity.to_xml
signature = xml.at_xpath("signature").text
expect(verify_signature(new_author_pkey, signature, entity.to_s)).to be_truthy
end
it "doesn't change signature if it is already set" do
hash[:signature] = "aa"
xml = Entities::AccountMigration.new(hash).to_xml
expect(xml.at_xpath("signature").text).to eq("aa")
end
it "raises when signature isn't set and key isn't supplied" do
expect_callback(:fetch_private_key, new_diaspora_id).and_return(nil)
it "fails validation on construction" do
expect {
Entities::AccountMigration.new(hash).to_xml
}.to raise_error Entities::AccountMigration::NewPrivateKeyNotFound
end
end
describe "#verify_signature" do
it "doesn't raise anything if correct signatures were passed" do
hash[:signature] = sign_with_key(new_author_pkey, signature_data)
expect_callback(:fetch_public_key, new_diaspora_id).and_return(new_author_pkey)
expect { Entities::AccountMigration.new(hash).verify_signature }.not_to raise_error
described_class.new(hash)
}.to raise_error Entity::ValidationError
end
it "raises when no public key for author was fetched" do
expect_callback(:fetch_public_key, anything).and_return(nil)
it "fails validation on parsing" do
expect {
Entities::AccountMigration.new(hash).verify_signature
}.to raise_error Entities::AccountMigration::PublicKeyNotFound
end
it "raises when bad author signature was passed" do
hash[:signature] = "abcdef"
expect_callback(:fetch_public_key, new_diaspora_id).and_return(new_author_pkey.public_key)
expect {
Entities::AccountMigration.new(hash).verify_signature
}.to raise_error Entities::AccountMigration::SignatureVerificationFailed
end
end
describe ".from_hash" do
it "calls #verify_signature" do
expect_any_instance_of(Entities::AccountMigration).to receive(:freeze)
expect_any_instance_of(Entities::AccountMigration).to receive(:verify_signature)
Entities::AccountMigration.from_hash(hash)
end
it "raises when bad author signature was passed" do
hash[:signature] = "abcdef"
expect_callback(:fetch_public_key, new_diaspora_id).and_return(new_author_pkey.public_key)
expect {
Entities::AccountMigration.from_hash(hash)
}.to raise_error Entities::AccountMigration::SignatureVerificationFailed
DiasporaFederation::Salmon::XmlPayload.unpack(Nokogiri::XML(xml).root)
}.to raise_error Entity::ValidationError
end
end
end

View file

@ -1,9 +1,6 @@
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
@ -27,51 +24,9 @@ module DiasporaFederation
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
it_behaves_like "a signable" do
let(:test_class) { TestSignableEntity }
let(:test_string) { TEST_STRING_VALUE }
end
end
end

View file

@ -8,12 +8,18 @@ module DiasporaFederation
let(:property) { :author }
end
describe "#person" do
describe "#profile" do
it_behaves_like "a property with a value validation/restriction" do
let(:property) { :profile }
let(:wrong_values) { [nil] }
let(:correct_values) { [] }
end
end
describe "#old_identity" do
it_behaves_like "a diaspora* ID validator" do
let(:property) { :old_identity }
end
end
end
end

View file

@ -0,0 +1,51 @@
shared_examples "a signable" do
let(:private_key) { OpenSSL::PKey::RSA.generate(1024) }
let(:test_signature) { sign_with_key(private_key, test_string) }
describe "#sign_with_key" do
it "produces a correct signature" do
signature = test_class.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 {
test_class
.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 {
test_class
.new(my_signature: test_signature)
.verify_signature("id@example.tld", :my_signature)
}.to raise_error(DiasporaFederation::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 {
test_class.new({}).verify_signature("id@example.tld", :my_signature)
}.to raise_error(DiasporaFederation::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 {
test_class
.new(my_signature: "faked signature")
.verify_signature("id@example.tld", :my_signature)
}.to raise_error(DiasporaFederation::Entities::Signable::SignatureVerificationFailed)
end
end
end