about summary refs log tree commit diff
path: root/app/controllers/concerns/signature_verification.rb
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2017-07-14 20:41:49 +0200
committerGitHub <noreply@github.com>2017-07-14 20:41:49 +0200
commit1618b68bfa740ed655ac45d7d5f4f46fed6c8c62 (patch)
tree1a82bc4cbbbab2a1ff3ce6743c64af4d8694cc38 /app/controllers/concerns/signature_verification.rb
parentc1f201c49a007e5c0740c00651e549a7b0416b05 (diff)
HTTP signatures (#4146)
* Add Request class with HTTP signature generator

Spec: https://tools.ietf.org/html/draft-cavage-http-signatures-06

* Add HTTP signature verification concern

* Add test for SignatureVerification concern

* Add basic test for Request class

* Make PuSH subscribe/unsubscribe requests use new Request class

Accidentally fix lease_seconds not being set and sent properly, and
change the new minimum subscription duration to 1 day

* Make all PuSH workers use new Request class

* Make Salmon sender use new Request class

* Make FetchLinkService use new Request class

* Make FetchAtomService use the new Request class

* Make Remotable use the new Request class

* Make ResolveRemoteAccountService use the new Request class

* Add more tests

* Allow +-30 seconds window for signed request to remain valid

* Disable time window validation for signed requests, restore 7 days
as PuSH subscription duration (which was previous default due to a bug)
Diffstat (limited to 'app/controllers/concerns/signature_verification.rb')
-rw-r--r--app/controllers/concerns/signature_verification.rb87
1 files changed, 87 insertions, 0 deletions
diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb
new file mode 100644
index 000000000..abe845d93
--- /dev/null
+++ b/app/controllers/concerns/signature_verification.rb
@@ -0,0 +1,87 @@
+# 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
+
+  def signed_request?
+    request.headers['Signature'].present?
+  end
+
+  def signed_request_account
+    return @signed_request_account if defined?(@signed_request_account)
+
+    unless signed_request?
+      @signed_request_account = nil
+      return
+    end
+
+    raw_signature    = request.headers['Signature']
+    signature_params = {}
+
+    raw_signature.split(',').each do |part|
+      parsed_parts = part.match(/([a-z]+)="([^"]+)"/i)
+      next if parsed_parts.nil? || parsed_parts.size != 3
+      signature_params[parsed_parts[1]] = parsed_parts[2]
+    end
+
+    if incompatible_signature?(signature_params)
+      @signed_request_account = nil
+      return
+    end
+
+    account = ResolveRemoteAccountService.new.call(signature_params['keyId'].gsub(/\Aacct:/, ''))
+
+    if account.nil?
+      @signed_request_account = nil
+      return
+    end
+
+    signature             = Base64.decode64(signature_params['signature'])
+    compare_signed_string = build_signed_string(signature_params['headers'])
+
+    if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
+      @signed_request_account = account
+      @signed_request_account
+    else
+      @signed_request_account = nil
+    end
+  end
+
+  private
+
+  def build_signed_string(signed_headers)
+    signed_headers = 'date' if signed_headers.blank?
+
+    signed_headers.split(' ').map do |signed_header|
+      if signed_header == Request::REQUEST_TARGET
+        "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
+      else
+        "#{signed_header}: #{request.headers[to_header_name(signed_header)]}"
+      end
+    end.join("\n")
+  end
+
+  def matches_time_window?
+    begin
+      time_sent = DateTime.httpdate(request.headers['Date'])
+    rescue ArgumentError
+      return false
+    end
+
+    (Time.now.utc - time_sent).abs <= 30
+  end
+
+  def to_header_name(name)
+    name.split(/-/).map(&:capitalize).join('-')
+  end
+
+  def incompatible_signature?(signature_params)
+    signature_params['keyId'].blank? ||
+      signature_params['signature'].blank? ||
+      signature_params['algorithm'].blank? ||
+      signature_params['algorithm'] != 'rsa-sha256' ||
+      !signature_params['keyId'].start_with?('acct:')
+  end
+end