fedipub 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e48fe0306a23dd822ac8e755412635303612686408512d9cd36139050fc961a7
4
- data.tar.gz: ca741f66e2d65b4501b9ae80465fde3500f81d65f55b56b0eef1d0a96bf2b97c
3
+ metadata.gz: 506f49df52ad99cb113b5526bace6666e2a916cfe13f0abcfe7afe49d08e295f
4
+ data.tar.gz: 1f9cb4fa11e555fec1ea7b0e9e0b600dc14b2d9ba9b31f5ebe7c63f22c554818
5
5
  SHA512:
6
- metadata.gz: e96c6dcb707777a477078d15b750e2257dafecdd7c489a21f2c8beea9c965be237da1ed414ac27327e684874d840d11274336106defb1d1fe43df242b6b274e8
7
- data.tar.gz: da1eca4e3c5d882a0dabec9668d521f1f9a707a6dfcbfe36d4ed6d42ffa761d0c7049ffc67d3d1666cae535d85af6ff80cdb11582ff4e6d068365e0949ed737c
6
+ metadata.gz: 915f0da60e1cdde3e9f10f3f3737e21063eacadc8ca5d7ace7f46a478aa0332f884d54df9796feecf4278278f856e8ffde2bba07fc0ea8ca06d347dcf600460a
7
+ data.tar.gz: e44d799ce12d87b68a89befa4135c06596554452b8ac37d74bda7213d841641854dfe1567c0ceb774991120fb3ed325f76fbdc3b32f5edac9816e77950793d53
data/README.md CHANGED
@@ -48,9 +48,14 @@ It _may_ work on other versions, but we won't provide support.
48
48
 
49
49
  Extensions add extra features to Fedipub:
50
50
 
51
- - [Fedipub Moderation](https://github.com/fedipub/fedipub-moderation/)
51
+ - [Fedipub Moderation](https://gitlab.com/fedipub/fedipub-moderation/)
52
52
  > A gem that provides trust & safety for Fedipub
53
53
 
54
+ ## Projects using Fedipub
55
+
56
+ - [Manyfold](https://github.com/manyfold3d/manyfold)
57
+ - [Prismo](https://github.com/mbajur/prismo)
58
+
54
59
  ## License
55
60
 
56
61
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -62,6 +67,7 @@ See [CONTRIBUTING](CONTRIBUTING.md) to have an overview of the process and the t
62
67
  ### Contributors
63
68
 
64
69
  - [echarp](https://gitlab.com/echarp)
70
+ - [Francesco Urdih](https://gitlab.com/francesco.urdih)
65
71
  - [James Smith](https://gitlab.com/floppy.uk)
66
72
  - [Manuel Tancoigne](https://gitlab.com/mtancoigne)
67
73
  - [pessi-v](https://github.com/pessi-v)
@@ -6,17 +6,16 @@ module Fedipub
6
6
  def find
7
7
  skip_authorization
8
8
 
9
- resource = params.require(:resource)
10
- case resource
9
+ case resource = params.require(:resource)
10
+ when %r{^https?://#{Fedipub::Utils::Host.localhost}/?$}
11
+ @actor = Fedipub::Actor.application_actor
11
12
  when %r{^https?://.+}
12
- @user = Fedipub::Actor.find_by_federation_url!(resource).entity # rubocop:disable Rails/DynamicFindBy
13
+ @actor = Fedipub::Actor.find_by_federation_url!(resource) # rubocop:disable Rails/DynamicFindBy
13
14
  when /^acct:.+/
14
- actor = Fedipub::Actor.find_local_by_username(username)
15
- raise Fedipub::Actor::TombstonedError if actor&.tombstoned?
16
-
17
- @user = actor&.entity
15
+ @actor = Fedipub::Actor.find_local_by_username(username)
16
+ raise Fedipub::Actor::TombstonedError if @actor&.tombstoned?
18
17
  end
19
- raise ActiveRecord::RecordNotFound if @user.nil?
18
+ raise ActiveRecord::RecordNotFound if @actor.nil?
20
19
 
21
20
  render formats: [:jrd]
22
21
  end
@@ -2,6 +2,7 @@ module Fedipub
2
2
  class ServerController < ::ActionController::Base # rubocop:disable Rails/ApplicationController
3
3
  include Pundit::Authorization
4
4
 
5
+ before_action :verify_request_signature!
5
6
  after_action :verify_authorized
6
7
 
7
8
  protect_from_forgery with: :null_session
@@ -12,8 +13,18 @@ module Fedipub
12
13
  Fedipub::DataEntity::TombstonedError,
13
14
  with: :error_gone
14
15
 
16
+ def self.require_signature?
17
+ false
18
+ end
19
+
15
20
  private
16
21
 
22
+ def verify_request_signature!
23
+ Fediverse::Signature.verify!(request: request, require_signature: ServerController.require_signature?)
24
+ rescue Fediverse::Signature::BadSignature
25
+ head :unauthorized
26
+ end
27
+
17
28
  def error_fallback(exception, fallback_message, status)
18
29
  message = exception&.message || fallback_message
19
30
  respond_to do |format|
@@ -38,7 +38,7 @@ module Fedipub
38
38
  #
39
39
  # @example
40
40
  # acts_as_fedipub_actor username_field: :username, name_field: :display_name, profile_url_method: :url_for, actor_type: 'Person'
41
- # rubocop:disable Metrics/ParameterLists
41
+ # rubocop:disable-next Metrics/ParameterLists
42
42
  def acts_as_fedipub_actor(
43
43
  name_field:,
44
44
  username_field:,
@@ -57,7 +57,6 @@ module Fedipub
57
57
  auto_create_actors: auto_create_actors
58
58
  )
59
59
  end
60
- # rubocop:enable Metrics/ParameterLists
61
60
 
62
61
  # Define a method that will be called after the entity receives a follow request.
63
62
  # The follow request will be passed as an argument to the method.
@@ -0,0 +1,25 @@
1
+ module Fedipub
2
+ # Definition for a single local Application Actor, which represents the instance/server itself.
3
+ #
4
+ # Keys from this actor are used to sign outgoing GET requests.
5
+ module ApplicationActor
6
+ extend ActiveSupport::Concern
7
+
8
+ APPLICATION_ACTOR_ATTRIBUTES = {
9
+ username: '__application',
10
+ actor_type: 'Application',
11
+ entity: nil,
12
+ local: true,
13
+ }.freeze
14
+
15
+ included do
16
+ def self.application_actor
17
+ find_or_create_by!(APPLICATION_ACTOR_ATTRIBUTES)
18
+ end
19
+
20
+ def application_actor?
21
+ APPLICATION_ACTOR_ATTRIBUTES.all? { |attribute, value| attributes[attribute.to_s] == value }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -126,7 +126,7 @@ module Fedipub
126
126
  #
127
127
  # @example
128
128
  # acts_as_fedipub_data handles: 'Note', with: :note_handler, route_path_segment: :articles, actor_entity_method: :user
129
- # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
129
+ # rubocop:disable-next Metrics/ParameterLists, Metrics/MethodLength
130
130
  def acts_as_fedipub_data(
131
131
  handles:,
132
132
  with: :handle_incoming_fediverse_data,
@@ -155,7 +155,6 @@ module Fedipub
155
155
  Fediverse::Inbox.register_handler 'Create', handles, self, with
156
156
  Fediverse::Inbox.register_handler 'Update', handles, self, with
157
157
  end
158
- # rubocop:enable Metrics/ParameterLists, Metrics/MethodLength
159
158
 
160
159
  # Instantiates a new instance from an ActivityPub object
161
160
  #
@@ -16,18 +16,17 @@ module Fedipub
16
16
  include Fedipub::HandlesDeleteRequests
17
17
  include Fedipub::Likeable
18
18
  include Fedipub::Announceable
19
+ include Fedipub::ApplicationActor
19
20
 
20
- validates :federated_url, presence: { unless: :entity }, uniqueness: { unless: :local? }
21
+ validates :federated_url, presence: { unless: -> { entity || application_actor? } }, uniqueness: { unless: :local? }
21
22
  validates :username, presence: { unless: :local? }
22
23
  validates :server, presence: { unless: :local? }
23
24
  validates :inbox_url, presence: { unless: :local? }
24
25
  validates :outbox_url, presence: { unless: :local? }
25
- validates :followers_url, presence: { unless: :local? }
26
- validates :followings_url, presence: { unless: :local? }
27
26
  validates :profile_url, presence: { unless: :local? }
28
- validates :actor_type, presence: { unless: :local? }
27
+ validates :actor_type, presence: { unless: -> { local? && !application_actor? } }
29
28
  validates :entity_id, uniqueness: { scope: :entity_type }, if: :entity_type
30
- validates :entity, presence: true, if: -> { local? && !tombstoned? }
29
+ validates :entity, presence: true, if: -> { local? && !tombstoned? && !application_actor? }
31
30
 
32
31
  belongs_to :entity, polymorphic: true, optional: true
33
32
  # FIXME: Handle this with something like undelete
@@ -59,7 +58,7 @@ module Fedipub
59
58
  end
60
59
 
61
60
  def federated_url
62
- use_entity_attributes? ? Fedipub::Engine.routes.url_helpers.server_actor_url(self) : attributes['federated_url'].presence
61
+ use_entity_attributes? || application_actor? ? Fedipub::Engine.routes.url_helpers.server_actor_url(self) : attributes['federated_url'].presence
63
62
  end
64
63
 
65
64
  def username
@@ -75,7 +74,7 @@ module Fedipub
75
74
  end
76
75
 
77
76
  def server
78
- use_entity_attributes? ? Utils::Host.localhost : attributes['server']
77
+ use_entity_attributes? || application_actor? ? Utils::Host.localhost : attributes['server']
79
78
  end
80
79
 
81
80
  def actor_type
@@ -83,19 +82,19 @@ module Fedipub
83
82
  end
84
83
 
85
84
  def inbox_url
86
- use_entity_attributes? ? Fedipub::Engine.routes.url_helpers.server_actor_inbox_url(self) : attributes['inbox_url']
85
+ use_entity_attributes? || application_actor? ? Fedipub::Engine.routes.url_helpers.server_actor_inbox_url(self) : attributes['inbox_url']
87
86
  end
88
87
 
89
88
  def outbox_url
90
- use_entity_attributes? ? Fedipub::Engine.routes.url_helpers.server_actor_outbox_url(self) : attributes['outbox_url']
89
+ use_entity_attributes? || application_actor? ? Fedipub::Engine.routes.url_helpers.server_actor_outbox_url(self) : attributes['outbox_url']
91
90
  end
92
91
 
93
92
  def followers_url
94
- use_entity_attributes? ? Fedipub::Engine.routes.url_helpers.followers_server_actor_url(self) : attributes['followers_url']
93
+ use_entity_attributes? || application_actor? ? Fedipub::Engine.routes.url_helpers.followers_server_actor_url(self) : attributes['followers_url']
95
94
  end
96
95
 
97
96
  def followings_url
98
- use_entity_attributes? ? Fedipub::Engine.routes.url_helpers.following_server_actor_url(self) : attributes['followings_url']
97
+ use_entity_attributes? || application_actor? ? Fedipub::Engine.routes.url_helpers.following_server_actor_url(self) : attributes['followings_url']
99
98
  end
100
99
 
101
100
  def profile_url
@@ -244,6 +243,10 @@ module Fedipub
244
243
  end
245
244
  return actor if actor
246
245
 
246
+ # Look for pure actors (e.g. the application actor)
247
+ actor = Fedipub::Actor.local.find_by username: username
248
+ return actor if actor
249
+
247
250
  # Last hope: Search for tombstoned actors
248
251
  Fedipub::Actor.local.tombstoned.find_by username: username
249
252
  end
@@ -25,3 +25,17 @@ if actor.public_key
25
25
  end
26
26
  end
27
27
  json.merge! actor_data
28
+
29
+ # FEP-844e capability discovery
30
+ if actor.application_actor?
31
+ json.implements([
32
+ 'https://www.w3.org/TR/activitypub/',
33
+ 'https://datatracker.ietf.org/doc/html/rfc9421',
34
+ 'https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12',
35
+ 'https://w3id.org/fep/844e',
36
+ 'https://w3id.org/fep/2677',
37
+ 'https://w3id.org/fep/d556',
38
+ ].map { |url| { 'href' => url } })
39
+ else
40
+ json.generator Fedipub::Actor.application_actor.federated_url
41
+ end
@@ -3,4 +3,8 @@ json.links [
3
3
  rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
4
4
  href: show_node_info_url,
5
5
  },
6
+ {
7
+ rel: 'https://www.w3.org/ns/activitystreams#Application',
8
+ href: Fedipub::Actor.application_actor.federated_url,
9
+ },
6
10
  ]
@@ -5,16 +5,24 @@ links = [
5
5
  {
6
6
  rel: 'self',
7
7
  type: Mime[:activitypub].to_s,
8
- href: @user.fedipub_actor.federated_url,
8
+ href: @actor.federated_url,
9
9
  },
10
10
  ]
11
11
 
12
+ if @actor.application_actor?
13
+ links << {
14
+ rel: 'https://www.w3.org/ns/activitystreams#Service',
15
+ type: Mime[:activitypub].to_s,
16
+ href: @actor.federated_url,
17
+ }
18
+ end
19
+
12
20
  # User profile URL if configured
13
21
  # TODO: Add a profile controller/action in dummy to test this
14
- if @user.fedipub_actor.profile_url
22
+ if @actor.profile_url
15
23
  links.push rel: 'https://webfinger.net/rel/profile-page',
16
24
  type: 'text/html',
17
- href: @user.fedipub_actor.profile_url
25
+ href: @actor.profile_url
18
26
  end
19
27
 
20
28
  # Remote following
@@ -1,41 +1,89 @@
1
1
  require 'faraday'
2
2
  require 'faraday/follow_redirects'
3
+ require 'fediverse/signature'
3
4
 
4
5
  module Fedipub
5
6
  module Utils
6
- # Simple wrapper to make requests expecting JSON
7
- module JsonRequest
8
- class UnhandledResponseStatus < StandardError; end
7
+ # Wrapper around HTTP calls which ensures signatures etc are applied.
8
+ class JsonRequest
9
+ include Singleton
10
+
11
+ class << self
12
+ extend Forwardable
13
+
14
+ def_delegators :instance, :get_json, :get, :post
15
+ end
9
16
 
10
- BASE_HEADERS = {
11
- 'Content-Type' => 'application/json',
12
- 'Accept' => 'application/json',
13
- }.freeze
17
+ class UnhandledResponseStatus < StandardError; end
14
18
 
15
19
  # Makes a GET request and returns a +Hash+ from the parsed body
16
20
  #
17
21
  # @param url [String] Target URL
18
22
  # @param params [Hash] Querystring parameters
19
23
  # @param headers [Hash] Additional headers
20
- # @param follow_redirects [Boolean] Whether to follow redirections
21
24
  # @param expected_status [Integer] Expected response status. Will raise a +UnhandledResponseStatus+ when status is different
22
25
  #
23
26
  # @return The parsed JSON object
24
27
  #
25
28
  # @raise [UnhandledResponseStatus] when response status is not the expected_status
26
- def self.get_json(url, params: {}, headers: {}, follow_redirects: false, expected_status: 200)
27
- headers = BASE_HEADERS.merge headers
28
-
29
- connection = Faraday.new url: url, params: params, headers: headers do |faraday|
30
- faraday.response :follow_redirects if follow_redirects
31
- faraday.adapter Faraday.default_adapter
32
- end
33
-
34
- response = connection.get
29
+ def get_json(url, params: {}, headers: {}, expected_status: 200, from: nil)
30
+ response = get url: url, params: params, headers: headers, from: from
35
31
  raise UnhandledResponseStatus, "Unhandled status code #{response.status} for GET #{url}" if expected_status && response.status != expected_status
36
32
 
37
33
  JSON.parse(response.body)
38
34
  end
35
+
36
+ def get(url:, params: {}, headers: {}, from: nil)
37
+ execute_request method: :get, url: url, params: params, headers: headers, from: from || Fedipub::Actor.application_actor
38
+ end
39
+
40
+ def post(url:, message:, headers: {}, from: nil)
41
+ execute_request method: :post, url: url, headers: headers, message: message, from: from
42
+ end
43
+
44
+ private
45
+
46
+ # Send to remote server with RFC9421 signature and double-knocking for draft-cavage-12 if that fails
47
+ def execute_request(method:, url:, params: {}, headers: {}, message: nil, from: nil) # rubocop:disable Metrics/ParameterLists
48
+ req = build_request(method: method, url: url, params: params, headers: headers, message: message)
49
+ response = connection.builder.build_response(
50
+ connection,
51
+ from ? Fediverse::Signature.sign(sender: from, request: req.dup) : req
52
+ )
53
+ # If signature was present and rejected, try double-knocking
54
+ return response unless from && response.status.in?([400, 401])
55
+
56
+ connection.builder.build_response(
57
+ connection,
58
+ Fediverse::Signature.sign(sender: from, request: req, legacy_signature: true)
59
+ )
60
+ end
61
+
62
+ def build_request(method:, url:, params: {}, headers: {}, message: nil) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
63
+ # Extract params from URL string if they're in there instead of the hash
64
+ uri = URI(url)
65
+ params.merge! Rack::Utils.parse_nested_query(uri.query)
66
+ uri.query = nil
67
+ # Build the request
68
+ connection.build_request(method) do |req|
69
+ req.url uri
70
+ req.body = message
71
+ req.params = params
72
+ req.headers = {
73
+ 'Content-Type' => Mime[:activitypub].to_s,
74
+ 'Accept' => [Mime[:activitypub].to_s, Mime[:activitypub].send(:synonyms), "#{Mime[:json]};q=0.5"].flatten.join(', '),
75
+ 'User-Agent' => req.headers['User-Agent'] || "Fedipub/#{Fedipub::VERSION}",
76
+ 'Accept-Signature' => 'sig1=()',
77
+ }.compact.merge(headers)
78
+ end
79
+ end
80
+
81
+ def connection
82
+ @connection ||= Faraday.new do |faraday|
83
+ faraday.response :follow_redirects
84
+ faraday.adapter Faraday.default_adapter
85
+ end
86
+ end
39
87
  end
40
88
  end
41
89
  end
@@ -1,3 +1,3 @@
1
1
  module Fedipub
2
- VERSION = '0.9.0'.freeze
2
+ VERSION = '0.10.0'.freeze
3
3
  end
@@ -28,7 +28,7 @@ module Fediverse
28
28
  end
29
29
 
30
30
  def nodeinfo_url(domain)
31
- response = Fedipub::Utils::JsonRequest.get_json "#{base_url(domain)}/.well-known/nodeinfo", follow_redirects: true
31
+ response = Fedipub::Utils::JsonRequest.get_json "#{base_url(domain)}/.well-known/nodeinfo"
32
32
  entry = response['links']&.find { |link| link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/2.0' }
33
33
 
34
34
  entry['href']
@@ -21,6 +21,11 @@ module Fediverse
21
21
 
22
22
  private
23
23
 
24
+ # Overridden by Fedipub::Moderation for filtering
25
+ def post_to_inbox(inbox_url:, message:, from: nil)
26
+ Fedipub::Utils::JsonRequest.post(url: inbox_url, message: message, from: from)
27
+ end
28
+
24
29
  # Determines the list of inboxes that the activity should be delivered to
25
30
  #
26
31
  # @return [Array<Fedipub::Actor>]
@@ -53,38 +58,6 @@ module Fediverse
53
58
  format: :json
54
59
  )
55
60
  end
56
-
57
- def post_to_inbox(inbox_url:, message:, from: nil)
58
- conn = Faraday.default_connection
59
- conn.builder.build_response(
60
- conn,
61
- signed_request(url: inbox_url, message: message, from: from)
62
- )
63
- end
64
-
65
- def signed_request(url:, message:, from:)
66
- req = request(url: url, message: message)
67
- req.headers['Signature'] = Fediverse::Signature.sign(sender: from, request: req) if from
68
- req
69
- end
70
-
71
- def request(url:, message:) # rubocop:todo Metrics/AbcSize
72
- Faraday.default_connection.build_request(:post) do |req|
73
- req.url url
74
- req.body = message
75
- req.headers['Content-Type'] = Mime[:activitypub].to_s
76
- req.headers['Accept'] = Mime[:activitypub].to_s
77
- req.headers['Host'] = URI.parse(url).host
78
- req.headers['Date'] = Time.now.utc.httpdate
79
- req.headers['Digest'] = digest(message)
80
- end
81
- end
82
-
83
- def digest(message)
84
- "SHA-256=#{Base64.strict_encode64(
85
- OpenSSL::Digest.new('SHA256').digest(message)
86
- )}"
87
- end
88
61
  end
89
62
  end
90
63
  end
@@ -0,0 +1,109 @@
1
+ require 'fediverse/signature'
2
+
3
+ module Fediverse
4
+ module Signature
5
+ class DraftCavage12
6
+ class << self
7
+ def sign(sender:, request:)
8
+ request = set_headers(request)
9
+
10
+ parts = {
11
+ keyId: sender.key_id,
12
+ headers: signature_headers(request).join(' '),
13
+ signature: signature(sender: sender, request: request),
14
+ }
15
+ request.headers['Signature'] = parts.map { |k, v| "#{k}=\"#{v}\"" }.join(',')
16
+ request
17
+ end
18
+
19
+ def verify!(request:)
20
+ # Do we have a signature to verify?
21
+ return false unless request.headers.key?('Signature')
22
+
23
+ # Have we got what we need?
24
+ components = signature_components(request)
25
+ raise Fediverse::Signature::BadSignature, 'Malformed signature' unless components['signature'] && components['headers']
26
+
27
+ # Find the sender
28
+ sender = find_sender_by_key_id(components['keyId'])
29
+ raise Fediverse::Signature::BadSignature, "Couldn't find sender" unless sender
30
+
31
+ # Build the expected payload
32
+ comparison_string = signature_payload(request: request, headers: components['headers'])
33
+
34
+ # Verify the payload against the signature
35
+ result = do_verification(components['signature'], sender, comparison_string)
36
+ raise Fediverse::Signature::BadSignature unless result
37
+
38
+ result
39
+ end
40
+
41
+ private
42
+
43
+ def do_verification(signature, sender, comparison_string)
44
+ signature = Base64.decode64(signature)
45
+ key = OpenSSL::PKey::RSA.new(sender.public_key)
46
+ key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison_string)
47
+ end
48
+
49
+ def find_sender_by_key_id(key_id)
50
+ return unless key_id
51
+
52
+ Fedipub::Actor.find_or_create_by_federation_url(
53
+ key_id.split('#', 2).first
54
+ )
55
+ end
56
+
57
+ def set_headers(request) # rubocop:disable Naming/AccessorMethodName
58
+ request.headers['Digest'] = digest(request.body) if request.body
59
+ request.headers['Host'] = URI.parse(request.path).host
60
+ request.headers['Date'] = Time.now.utc.httpdate
61
+ request
62
+ end
63
+
64
+ def signature_components(request)
65
+ request.headers['Signature'].split(',').to_h do |pair|
66
+ /\A(?<key>\w+)="(?<value>.*)"\z/ =~ pair
67
+ [key, value]
68
+ end
69
+ end
70
+
71
+ def digest(message)
72
+ "SHA-256=#{Base64.strict_encode64(
73
+ OpenSSL::Digest.new('SHA256').digest(message)
74
+ )}"
75
+ end
76
+
77
+ def signature_payload(request:, headers: signature_headers(request))
78
+ headers = headers.split if headers.is_a?(String)
79
+ headers.map do |header|
80
+ case header
81
+ when '(request-target)'
82
+ "(request-target): #{(request.try(:http_method) || request.try(:method)).downcase} #{URI.parse(request.path).path}"
83
+ else
84
+ "#{header}: #{request.headers[header.capitalize]}"
85
+ end
86
+ end.join("\n")
87
+ end
88
+
89
+ def signature_headers(request)
90
+ if request.body
91
+ %w[(request-target) host date digest]
92
+ else
93
+ %w[(request-target) host date]
94
+ end
95
+ end
96
+
97
+ def signature(sender:, request:)
98
+ private_key = OpenSSL::PKey::RSA.new sender.private_key, Rails.application.credentials.secret_key_base
99
+ Base64.strict_encode64(
100
+ private_key.sign(
101
+ OpenSSL::Digest.new('SHA256'),
102
+ signature_payload(request: request)
103
+ )
104
+ )
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,69 @@
1
+ require 'linzer'
2
+ require 'linzer/faraday'
3
+ require 'linzer/rack'
4
+
5
+ Linzer::Message.register_adapter ActionDispatch::Request, Linzer::Message::Adapter::Rack::Request
6
+
7
+ module Fediverse
8
+ module Signature
9
+ class Rfc9421
10
+ class << self
11
+ def sign(sender:, request:)
12
+ request.headers['Content-Digest'] = digest(request.body) if request.body
13
+ Linzer.sign!(
14
+ request,
15
+ key: linzer_private_key(sender),
16
+ components: components(request),
17
+ params: {
18
+ created: Time.now.to_i,
19
+ alg: 'rsa-v1_5-sha256',
20
+ }
21
+ )
22
+ request
23
+ end
24
+
25
+ def verify!(request:)
26
+ # Do we have a signature to verify?
27
+ return false if !request.headers.key?('Signature-Input') || !request.headers.key?('Signature')
28
+
29
+ # Verify the signature
30
+ Linzer.verify!(request) do |key_id|
31
+ sender = Fedipub::Actor.find_or_create_by_federation_url(key_id.split('#', 2).first)
32
+ raise Fediverse::Signature::BadSignature if sender.nil?
33
+
34
+ linzer_public_key(sender)
35
+ end
36
+ rescue Linzer::Error
37
+ raise Fediverse::Signature::BadSignature
38
+ end
39
+
40
+ private
41
+
42
+ # Converts key to right structure for Linzer to use
43
+ def linzer_private_key(sender)
44
+ private_key = OpenSSL::PKey::RSA.new sender.private_key, Rails.application.credentials.secret_key_base
45
+ Linzer.new_rsa_v1_5_sha256_key(private_key.to_pem, sender.key_id)
46
+ end
47
+
48
+ def linzer_public_key(sender)
49
+ public_key = OpenSSL::PKey::RSA.new(sender.public_key)
50
+ Linzer.new_rsa_v1_5_sha256_key(public_key.to_pem, sender.key_id)
51
+ end
52
+
53
+ def components(request)
54
+ if request.body
55
+ %w[@method @target-uri content-digest]
56
+ else
57
+ %w[@method @target-uri]
58
+ end
59
+ end
60
+
61
+ def digest(message)
62
+ "sha-256=:#{Base64.strict_encode64(
63
+ OpenSSL::Digest.new('SHA256').digest(message)
64
+ )}:"
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -1,49 +1,26 @@
1
1
  module Fediverse
2
- class Signature
3
- class << self
4
- def sign(sender:, request:)
5
- private_key = OpenSSL::PKey::RSA.new sender.private_key, Rails.application.credentials.secret_key_base
6
- headers = '(request-target) host date digest'
7
- sig = Base64.strict_encode64(
8
- private_key.sign(
9
- OpenSSL::Digest.new('SHA256'), signature_payload(request: request, headers: headers)
10
- )
11
- )
12
- {
13
- keyId: sender.key_id,
14
- headers: headers,
15
- signature: sig,
16
- }.map { |k, v| "#{k}=\"#{v}\"" }.join(',')
17
- end
2
+ module Signature
3
+ class BadSignature < StandardError; end
18
4
 
19
- def verify(sender:, request:)
20
- raise 'Unsigned headers' unless request.headers['Signature']
21
-
22
- signature_header = request.headers['Signature'].split(',').to_h do |pair|
23
- /\A(?<key>\w+)="(?<value>.*)"\z/ =~ pair
24
- [key, value]
5
+ class << self
6
+ def sign(sender:, request:, legacy_signature: false)
7
+ if legacy_signature
8
+ Fediverse::Signature::DraftCavage12.sign(sender: sender, request: request)
9
+ else
10
+ Fediverse::Signature::Rfc9421.sign(sender: sender, request: request)
25
11
  end
26
-
27
- headers = signature_header['headers']
28
- signature = Base64.decode64(signature_header['signature'])
29
- key = OpenSSL::PKey::RSA.new(sender.public_key)
30
-
31
- comparison_string = signature_payload(request: request, headers: headers)
32
-
33
- key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison_string)
34
12
  end
35
13
 
36
- private
14
+ def verify!(request:, require_signature: false) # rubocop:disable Naming/PredicateMethod
15
+ success = Fediverse::Signature::Rfc9421.verify!(request: request) ||
16
+ Fediverse::Signature::DraftCavage12.verify!(request: request)
17
+ raise(BadSignature) if require_signature && !success
37
18
 
38
- def signature_payload(request:, headers:)
39
- headers.split.map do |signed_header_name|
40
- if signed_header_name == '(request-target)'
41
- "(request-target): #{request.http_method} #{URI.parse(request.path).path}"
42
- else
43
- "#{signed_header_name}: #{request.headers[signed_header_name.capitalize]}"
44
- end
45
- end.join("\n")
19
+ true
46
20
  end
47
21
  end
48
22
  end
49
23
  end
24
+
25
+ require 'fediverse/signature/draft_cavage12'
26
+ require 'fediverse/signature/rfc9421'
@@ -121,7 +121,7 @@ module Fediverse
121
121
  # @return [Hash]
122
122
  # @raise [ActiveRecord::RecordNotFound] when the response is invalid
123
123
  def get_json(url, params = {})
124
- Fedipub::Utils::JsonRequest.get_json(url, params: params, follow_redirects: true, headers: { accept: 'application/json' })
124
+ Fedipub::Utils::JsonRequest.get_json(url, params: params, headers: { accept: 'application/json' })
125
125
  rescue Fedipub::Utils::JsonRequest::UnhandledResponseStatus => e
126
126
  Rails.logger.debug { e.message }
127
127
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fedipub
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Manuel Tancoigne
@@ -93,6 +93,20 @@ dependencies:
93
93
  - - ">="
94
94
  - !ruby/object:Gem::Version
95
95
  version: 1.2.0
96
+ - !ruby/object:Gem::Dependency
97
+ name: linzer
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.8'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.8'
96
110
  - !ruby/object:Gem::Dependency
97
111
  name: ostruct
98
112
  requirement: !ruby/object:Gem::Requirement
@@ -166,6 +180,7 @@ files:
166
180
  - app/mailers/fedipub/application_mailer.rb
167
181
  - app/models/concerns/fedipub/actor_entity.rb
168
182
  - app/models/concerns/fedipub/announceable.rb
183
+ - app/models/concerns/fedipub/application_actor.rb
169
184
  - app/models/concerns/fedipub/data_entity.rb
170
185
  - app/models/concerns/fedipub/handles_delete_requests.rb
171
186
  - app/models/concerns/fedipub/has_uuid.rb
@@ -260,6 +275,8 @@ files:
260
275
  - lib/fediverse/notifier.rb
261
276
  - lib/fediverse/request.rb
262
277
  - lib/fediverse/signature.rb
278
+ - lib/fediverse/signature/draft_cavage12.rb
279
+ - lib/fediverse/signature/rfc9421.rb
263
280
  - lib/fediverse/webfinger.rb
264
281
  - lib/generators/fedipub/copy_client_policies/USAGE
265
282
  - lib/generators/fedipub/copy_client_policies/copy_client_policies_generator.rb