2017-07-14 20:41:49 +02:00
|
|
|
# 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
|
|
|
|
|
|
2019-07-09 03:27:35 +02:00
|
|
|
include DomainControlHelper
|
|
|
|
|
|
Add support for latest HTTP Signatures spec draft (#14556)
* Add support for latest HTTP Signatures spec draft
https://www.ietf.org/id/draft-ietf-httpbis-message-signatures-00.html
- add support for the “hs2019” signature algorithm (assumed to be equivalent
to RSA-SHA256, since we do not have a mechanism to specify the algorithm
within the key metadata yet)
- add support for (created) and (expires) pseudo-headers and related
signature parameters, when using the hs2019 signature algorithm
- adjust default “headers” parameter while being backwards-compatible with
previous implementation
- change the acceptable time window logic from 12 hours surrounding the “date”
header to accepting signatures created up to 1 hour in the future and
expiring up to 1 hour in the past (but only allowing expiration dates up to
12 hours after the creation date)
This doesn't conform with the current draft, as it doesn't permit accounting
for clock skew.
This, however, should be addressed in a next version of the draft:
https://github.com/httpwg/http-extensions/pull/1235
* Add additional signature requirements
* Rewrite signature params parsing using Parslet
* Make apparent which signature algorithm Mastodon on verification failure
Mastodon uses RSASSA-PKCS1-v1_5, which is not recommended for new applications,
and new implementers may thus unknowingly use RSASSA-PSS.
* Add workaround for PeerTube's invalid signature header
The previous parser allowed incorrect Signature headers, such as
those produced by old versions of the `http-signature` node.js package,
and seemingly used by PeerTube.
This commit adds a workaround for that.
* Fix `signature_key_id` raising an exception
Previously, parsing failures would result in `signature_key_id` being nil,
but the parser changes made that result in an exception.
This commit changes the `signature_key_id` method to return `nil` in case
of parsing failures.
* Move extra HTTP signature helper methods to private methods
* Relax (request-target) requirement to (request-target) || digest
This lets requests from Plume work without lowering security significantly.
2020-08-24 18:21:07 +02:00
|
|
|
EXPIRATION_WINDOW_LIMIT = 12.hours
|
|
|
|
|
CLOCK_SKEW_MARGIN = 1.hour
|
2025-08-12 04:15:22 -04:00
|
|
|
STOPLIGHT_COOL_OFF_TIME = 5.minutes.seconds
|
|
|
|
|
STOPLIGHT_THRESHOLD = 1
|
Add support for latest HTTP Signatures spec draft (#14556)
* Add support for latest HTTP Signatures spec draft
https://www.ietf.org/id/draft-ietf-httpbis-message-signatures-00.html
- add support for the “hs2019” signature algorithm (assumed to be equivalent
to RSA-SHA256, since we do not have a mechanism to specify the algorithm
within the key metadata yet)
- add support for (created) and (expires) pseudo-headers and related
signature parameters, when using the hs2019 signature algorithm
- adjust default “headers” parameter while being backwards-compatible with
previous implementation
- change the acceptable time window logic from 12 hours surrounding the “date”
header to accepting signatures created up to 1 hour in the future and
expiring up to 1 hour in the past (but only allowing expiration dates up to
12 hours after the creation date)
This doesn't conform with the current draft, as it doesn't permit accounting
for clock skew.
This, however, should be addressed in a next version of the draft:
https://github.com/httpwg/http-extensions/pull/1235
* Add additional signature requirements
* Rewrite signature params parsing using Parslet
* Make apparent which signature algorithm Mastodon on verification failure
Mastodon uses RSASSA-PKCS1-v1_5, which is not recommended for new applications,
and new implementers may thus unknowingly use RSASSA-PSS.
* Add workaround for PeerTube's invalid signature header
The previous parser allowed incorrect Signature headers, such as
those produced by old versions of the `http-signature` node.js package,
and seemingly used by PeerTube.
This commit adds a workaround for that.
* Fix `signature_key_id` raising an exception
Previously, parsing failures would result in `signature_key_id` being nil,
but the parser changes made that result in an exception.
This commit changes the `signature_key_id` method to return `nil` in case
of parsing failures.
* Move extra HTTP signature helper methods to private methods
* Relax (request-target) requirement to (request-target) || digest
This lets requests from Plume work without lowering security significantly.
2020-08-24 18:21:07 +02:00
|
|
|
|
2022-09-21 22:45:57 +02:00
|
|
|
def require_account_signature!
|
2023-01-18 16:47:56 +01:00
|
|
|
render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
|
2019-07-11 20:11:09 +02:00
|
|
|
end
|
|
|
|
|
|
2022-09-21 22:45:57 +02:00
|
|
|
def require_actor_signature!
|
2023-01-18 16:47:56 +01:00
|
|
|
render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_actor
|
2022-09-21 22:45:57 +02:00
|
|
|
end
|
|
|
|
|
|
2017-07-14 20:41:49 +02:00
|
|
|
def signed_request?
|
|
|
|
|
request.headers['Signature'].present?
|
|
|
|
|
end
|
|
|
|
|
|
2025-06-02 11:27:08 +02:00
|
|
|
def signature_key_id
|
|
|
|
|
signed_request.key_id
|
2025-06-13 15:36:33 +02:00
|
|
|
rescue Mastodon::SignatureVerificationError
|
|
|
|
|
nil
|
2025-06-02 11:27:08 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
|
|
def signed_request
|
|
|
|
|
@signed_request ||= SignedRequest.new(request) if signed_request?
|
|
|
|
|
end
|
|
|
|
|
|
2017-10-03 23:21:19 +02:00
|
|
|
def signature_verification_failure_reason
|
2019-07-11 20:11:09 +02:00
|
|
|
@signature_verification_failure_reason
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def signature_verification_failure_code
|
|
|
|
|
@signature_verification_failure_code || 401
|
2017-10-03 23:21:19 +02:00
|
|
|
end
|
|
|
|
|
|
2017-07-14 20:41:49 +02:00
|
|
|
def signed_request_account
|
2022-09-21 22:45:57 +02:00
|
|
|
signed_request_actor.is_a?(Account) ? signed_request_actor : nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def signed_request_actor
|
|
|
|
|
return @signed_request_actor if defined?(@signed_request_actor)
|
2017-07-14 20:41:49 +02:00
|
|
|
|
2025-04-02 09:54:29 +02:00
|
|
|
raise Mastodon::SignatureVerificationError, 'Request not signed' unless signed_request?
|
2017-07-14 20:41:49 +02:00
|
|
|
|
2025-06-02 11:27:08 +02:00
|
|
|
actor = actor_from_key_id
|
2017-07-14 20:41:49 +02:00
|
|
|
|
2025-06-02 11:27:08 +02:00
|
|
|
raise Mastodon::SignatureVerificationError, "Public key not found for key #{signature_key_id}" if actor.nil?
|
2017-07-14 20:41:49 +02:00
|
|
|
|
2025-06-02 11:27:08 +02:00
|
|
|
return (@signed_request_actor = actor) if signed_request.verified?(actor)
|
2024-01-03 12:29:26 +01:00
|
|
|
|
2024-04-02 11:47:40 -04:00
|
|
|
actor = stoplight_wrapper.run { actor_refresh_key!(actor) }
|
2019-01-07 21:45:13 +01:00
|
|
|
|
2025-06-02 11:27:08 +02:00
|
|
|
raise Mastodon::SignatureVerificationError, "Could not refresh public key #{signature_key_id}" if actor.nil?
|
2024-01-03 12:29:26 +01:00
|
|
|
|
2025-06-02 11:27:08 +02:00
|
|
|
return (@signed_request_actor = actor) if signed_request.verified?(actor)
|
2019-01-07 21:45:13 +01:00
|
|
|
|
2025-06-02 11:27:08 +02:00
|
|
|
fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri}"
|
2025-07-08 11:31:04 +02:00
|
|
|
rescue Mastodon::MalformedHeaderError => e
|
|
|
|
|
@signature_verification_failure_code = 400
|
|
|
|
|
fail_with! e.message
|
2025-04-02 09:54:29 +02:00
|
|
|
rescue Mastodon::SignatureVerificationError => e
|
2022-09-20 23:30:26 +02:00
|
|
|
fail_with! e.message
|
2024-10-08 10:59:51 -04:00
|
|
|
rescue *Mastodon::HTTP_CONNECTION_ERRORS => e
|
2025-12-12 13:42:43 +01:00
|
|
|
@signature_verification_failure_code ||= 503
|
2022-09-20 23:30:26 +02:00
|
|
|
fail_with! "Failed to fetch remote data: #{e.message}"
|
2022-09-21 14:48:35 +02:00
|
|
|
rescue Mastodon::UnexpectedResponseError
|
2025-12-12 13:42:43 +01:00
|
|
|
@signature_verification_failure_code ||= 503
|
2022-09-20 23:30:26 +02:00
|
|
|
fail_with! 'Failed to fetch remote data (got unexpected reply from server)'
|
|
|
|
|
rescue Stoplight::Error::RedLight
|
2025-12-12 13:42:43 +01:00
|
|
|
@signature_verification_failure_code ||= 503
|
2022-09-20 23:30:26 +02:00
|
|
|
fail_with! 'Fetching attempt skipped because of recent connection failure'
|
2017-07-14 20:41:49 +02:00
|
|
|
end
|
|
|
|
|
|
2023-01-18 16:47:56 +01:00
|
|
|
def fail_with!(message, **options)
|
2023-09-06 12:17:22 +02:00
|
|
|
Rails.logger.debug { "Signature verification failed: #{message}" }
|
2023-08-29 10:29:07 +02:00
|
|
|
|
2023-01-18 16:47:56 +01:00
|
|
|
@signature_verification_failure_reason = { error: message }.merge(options)
|
2022-09-21 22:45:57 +02:00
|
|
|
@signed_request_actor = nil
|
2022-09-20 23:30:26 +02:00
|
|
|
end
|
|
|
|
|
|
2025-06-02 11:27:08 +02:00
|
|
|
def actor_from_key_id
|
2025-06-20 11:44:26 +02:00
|
|
|
key_id = signed_request.key_id
|
2019-07-11 20:11:09 +02:00
|
|
|
domain = key_id.start_with?('acct:') ? key_id.split('@').last : key_id
|
|
|
|
|
|
|
|
|
|
if domain_not_allowed?(domain)
|
|
|
|
|
@signature_verification_failure_code = 403
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
2017-08-09 23:54:14 +02:00
|
|
|
if key_id.start_with?('acct:')
|
2024-04-02 11:47:40 -04:00
|
|
|
stoplight_wrapper.run { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) }
|
2017-08-09 23:54:14 +02:00
|
|
|
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
|
2022-09-21 22:45:57 +02:00
|
|
|
account = ActivityPub::TagManager.instance.uri_to_actor(key_id)
|
2024-04-02 11:47:40 -04:00
|
|
|
account ||= stoplight_wrapper.run { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) }
|
2017-08-21 22:57:34 +02:00
|
|
|
account
|
2017-08-09 23:54:14 +02:00
|
|
|
end
|
2022-09-20 23:30:26 +02:00
|
|
|
rescue Mastodon::PrivateNetworkAddressError => e
|
2025-04-02 09:54:29 +02:00
|
|
|
raise Mastodon::SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
|
2022-09-21 22:45:57 +02:00
|
|
|
rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e
|
2025-04-02 09:54:29 +02:00
|
|
|
raise Mastodon::SignatureVerificationError, e.message
|
2017-07-14 20:41:49 +02:00
|
|
|
end
|
2019-01-07 21:45:13 +01:00
|
|
|
|
2024-04-02 11:47:40 -04:00
|
|
|
def stoplight_wrapper
|
2025-08-12 04:15:22 -04:00
|
|
|
Stoplight(
|
|
|
|
|
"source:#{request.remote_ip}",
|
|
|
|
|
cool_off_time: STOPLIGHT_COOL_OFF_TIME,
|
|
|
|
|
threshold: STOPLIGHT_THRESHOLD,
|
|
|
|
|
tracked_errors: [HTTP::Error, OpenSSL::SSL::SSLError]
|
|
|
|
|
)
|
2019-05-23 15:22:39 +02:00
|
|
|
end
|
|
|
|
|
|
2022-09-21 22:45:57 +02:00
|
|
|
def actor_refresh_key!(actor)
|
|
|
|
|
return if actor.local? || !actor.activitypub?
|
|
|
|
|
return actor.refresh! if actor.respond_to?(:refresh!) && actor.possibly_stale?
|
|
|
|
|
|
|
|
|
|
ActivityPub::FetchRemoteActorService.new.call(actor.uri, only_key: true, suppress_errors: false)
|
2022-09-20 23:30:26 +02:00
|
|
|
rescue Mastodon::PrivateNetworkAddressError => e
|
2025-04-02 09:54:29 +02:00
|
|
|
raise Mastodon::SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
|
2022-09-21 22:45:57 +02:00
|
|
|
rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, Webfinger::Error => e
|
2025-04-02 09:54:29 +02:00
|
|
|
raise Mastodon::SignatureVerificationError, e.message
|
2019-01-07 21:45:13 +01:00
|
|
|
end
|
2017-07-14 20:41:49 +02:00
|
|
|
end
|