-
-
Notifications
You must be signed in to change notification settings - Fork 7.5k
Expand file tree
/
Copy pathsignature_verification.rb
More file actions
161 lines (124 loc) · 5.81 KB
/
Copy pathsignature_verification.rb
File metadata and controls
161 lines (124 loc) · 5.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# frozen_string_literal: true
# Implemented according to HTTP signatures (Draft 6)
# <https://tools.ietf.org/html/draft-cavage-http-signatures-06>
module SignatureVerification
extend ActiveSupport::Concern
include DomainControlHelper
STOPLIGHT_COOL_OFF_TIME = 5.minutes.seconds
STOPLIGHT_THRESHOLD = 1
def require_account_signature!
render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
end
def require_actor_signature!
render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_actor
end
def signed_request?
request.headers['Signature'].present?
end
def signature_key_id
signed_request.key_id
rescue Mastodon::SignatureVerificationError
nil
end
private
def signed_request
@signed_request ||= SignedRequest.new(request) if signed_request?
end
def signature_verification_failure_reason
@signature_verification_failure_reason
end
def signature_verification_failure_code
@signature_verification_failure_code || 401
end
def signed_request_account
signed_request_actor.is_a?(Account) ? signed_request_actor : nil
end
def signed_request_actor
return @signed_request_actor if defined?(@signed_request_actor)
raise Mastodon::SignatureVerificationError, 'Request not signed' unless signed_request?
keypair = keypair_from_key_id
raise Mastodon::SignatureVerificationError, "Public key not found for key #{signature_key_id}" if keypair.nil?
check_keypair_validity!(keypair)
return (@signed_request_actor = keypair.actor) if signed_request.verified?(keypair)
keypair = stoplight_wrapper.run { keypair_refresh_key!(keypair) }
raise Mastodon::SignatureVerificationError, "Could not refresh public key #{signature_key_id}" if keypair.nil?
check_keypair_validity!(keypair)
return (@signed_request_actor = keypair.actor) if signed_request.verified?(keypair)
fail_with! "Verification failed for #{keypair.actor.to_log_human_identifier} #{keypair.actor.uri} #{keypair.uri}"
rescue Mastodon::MalformedHeaderError => e
@signature_verification_failure_code = 400
fail_with! e.message
rescue Mastodon::SignatureVerificationError => e
fail_with! e.message
rescue *Mastodon::HTTP_CONNECTION_ERRORS => e
@signature_verification_failure_code ||= 503
fail_with! "Failed to fetch remote data: #{e.message}"
rescue Mastodon::UnexpectedResponseError
@signature_verification_failure_code ||= 503
fail_with! 'Failed to fetch remote data (got unexpected reply from server)'
rescue Stoplight::Error::RedLight
@signature_verification_failure_code ||= 503
fail_with! 'Fetching attempt skipped because of recent connection failure'
end
def fail_with!(message, **options)
Rails.logger.debug { "Signature verification failed: #{message}" }
@signature_verification_failure_reason = { error: message }.merge(options)
@signed_request_actor = nil
end
def keypair_from_key_id
key_id = signed_request.key_id
domain = key_id.start_with?('acct:') ? key_id.split('@').last : key_id
if domain_not_allowed?(domain)
@signature_verification_failure_code = 403
return
end
if key_id.start_with?('acct:')
stoplight_wrapper.run { fetch_key_from_acct(key_id.delete_prefix('acct:')) }
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
keypair = Keypair.from_keyid(key_id)
return keypair if keypair.present?
stoplight_wrapper.run { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) }
end
rescue Mastodon::PrivateNetworkAddressError => e
raise Mastodon::SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
rescue Mastodon::HostValidationError, ActivityPub::ProcessAccountService::Error, ActivityPub::FetchRemoteActorService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e
raise Mastodon::SignatureVerificationError, e.message
end
def fetch_key_from_acct(acct)
# This is legacy and can't let us pick a specific key, so pick the first
account = ResolveAccountService.new.call(acct, suppress_errors: false)
return if account.nil?
account.keypairs.first || Keypair.from_legacy_account(account)
end
def stoplight_wrapper
Stoplight(
"source:#{request.remote_ip}",
cool_off_time: STOPLIGHT_COOL_OFF_TIME,
threshold: STOPLIGHT_THRESHOLD,
tracked_errors: [HTTP::Error, OpenSSL::SSL::SSLError]
)
end
def keypair_refresh_key!(keypair)
return if keypair.actor.local? || !keypair.actor.activitypub?
actor = if keypair.actor.possibly_stale?
# Doing a full profile refresh
keypair.actor.refresh!
else
# Only refreshing keys, skipping potentially more expensive requests
ActivityPub::FetchRemoteActorService.new.call(keypair.actor.uri, only_key: true, suppress_errors: false)
end
return if actor.nil?
keypair_uri = keypair.uri
keypair = actor.keypairs.find_by(uri: keypair_uri)
return keypair if keypair.present?
Keypair.from_legacy_account(actor, uri: keypair_uri) if actor.public_key.present?
rescue Mastodon::PrivateNetworkAddressError => e
raise Mastodon::SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, Webfinger::Error => e
raise Mastodon::SignatureVerificationError, e.message
end
def check_keypair_validity!(keypair)
raise Mastodon::SignatureVerificationError, "Key #{signature_key_id} is revoked" if keypair.revoked?
raise Mastodon::SignatureVerificationError, "Key #{signature_key_id} has expired" if keypair.expired?
end
end