Skip to content

Commit f99753b

Browse files
ClearlyClairehiyuki2578
authored andcommitted
Improvements to signature verification (mastodon#9667)
* Refactor signature verification a bit * Rescue signature verification if recorded public key is invalid Fixes mastodon#8822 * Always re-fetch AP signing key when HTTP Signature verification fails But when the account is not marked as stale, avoid fetching collections and media, and avoid webfinger round-trip. * Apply stoplight to key/account update as well as initial key retrieval
1 parent 8cf5c17 commit f99753b

3 files changed

Lines changed: 41 additions & 22 deletions

File tree

app/controllers/concerns/signature_verification.rb

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,23 +60,26 @@ def signed_request_account
6060
signature = Base64.decode64(signature_params['signature'])
6161
compare_signed_string = build_signed_string(signature_params['headers'])
6262

63-
if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
64-
@signed_request_account = account
65-
@signed_request_account
66-
elsif account.possibly_stale?
67-
account = account.refresh!
63+
return account unless verify_signature(account, signature, compare_signed_string).nil?
6864

69-
if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
70-
@signed_request_account = account
71-
@signed_request_account
72-
else
73-
@signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri}"
74-
@signed_request_account = nil
75-
end
76-
else
77-
@signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri}"
65+
account_stoplight = Stoplight("source:#{request.ip}") { account.possibly_stale? ? account.refresh! : account_refresh_key(account) }
66+
.with_fallback { nil }
67+
.with_threshold(1)
68+
.with_cool_off_time(5.minutes.seconds)
69+
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) }
70+
71+
account = account_stoplight.run
72+
73+
if account.nil?
74+
@signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
7875
@signed_request_account = nil
76+
return
7977
end
78+
79+
return account unless verify_signature(account, signature, compare_signed_string).nil?
80+
81+
@signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri}"
82+
@signed_request_account = nil
8083
end
8184

8285
def request_body
@@ -85,6 +88,15 @@ def request_body
8588

8689
private
8790

91+
def verify_signature(account, signature, compare_signed_string)
92+
if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
93+
@signed_request_account = account
94+
@signed_request_account
95+
end
96+
rescue OpenSSL::PKey::RSAError
97+
nil
98+
end
99+
88100
def build_signed_string(signed_headers)
89101
signed_headers = 'date' if signed_headers.blank?
90102

@@ -131,4 +143,9 @@ def account_from_key_id(key_id)
131143
account
132144
end
133145
end
146+
147+
def account_refresh_key(account)
148+
return if account.local? || !account.activitypub?
149+
ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true)
150+
end
134151
end

app/services/activitypub/fetch_remote_account_service.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ class ActivityPub::FetchRemoteAccountService < BaseService
55

66
SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
77

8-
# Does a WebFinger roundtrip on each call
9-
def call(uri, id: true, prefetched_body: nil, break_on_redirect: false)
8+
# Does a WebFinger roundtrip on each call, unless `only_key` is true
9+
def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false)
1010
return ActivityPub::TagManager.instance.uri_to_resource(uri, Account) if ActivityPub::TagManager.instance.local_uri?(uri)
1111

1212
@json = if prefetched_body.nil?
@@ -21,9 +21,9 @@ def call(uri, id: true, prefetched_body: nil, break_on_redirect: false)
2121
@username = @json['preferredUsername']
2222
@domain = Addressable::URI.parse(@uri).normalized_host
2323

24-
return unless verified_webfinger?
24+
return unless only_key || verified_webfinger?
2525

26-
ActivityPub::ProcessAccountService.new.call(@username, @domain, @json)
26+
ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key)
2727
rescue Oj::ParseError
2828
nil
2929
end

app/services/activitypub/process_account_service.rb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ def call(username, domain, json, options = {})
3333

3434
after_protocol_change! if protocol_changed?
3535
after_key_change! if key_changed? && !@options[:signed_with_known_key]
36-
check_featured_collection! if @account.featured_collection_url.present?
37-
check_links! unless @account.fields.empty?
36+
unless @options[:only_key]
37+
check_featured_collection! if @account.featured_collection_url.present?
38+
check_links! unless @account.fields.empty?
39+
end
3840

3941
@account
4042
rescue Oj::ParseError
@@ -54,11 +56,11 @@ def create_account
5456
end
5557

5658
def update_account
57-
@account.last_webfingered_at = Time.now.utc
59+
@account.last_webfingered_at = Time.now.utc unless @options[:only_key]
5860
@account.protocol = :activitypub
5961

6062
set_immediate_attributes!
61-
set_fetchable_attributes!
63+
set_fetchable_attributes! unless @options[:only_keys]
6264

6365
@account.save_with_optional_media!
6466
end

0 commit comments

Comments
 (0)