linked_data_signature.rb 2.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. # frozen_string_literal: true
  2. class ActivityPub::LinkedDataSignature
  3. include JsonLdHelper
  4. CONTEXT = 'https://w3id.org/identity/v1'
  5. SIGNATURE_CONTEXT = 'https://w3id.org/security/v1'
  6. def initialize(json)
  7. @json = json.with_indifferent_access
  8. end
  9. def verify_actor!
  10. return unless @json['signature'].is_a?(Hash)
  11. type = @json['signature']['type']
  12. creator_uri = @json['signature']['creator']
  13. signature = @json['signature']['signatureValue']
  14. return unless type == 'RsaSignature2017'
  15. creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri)
  16. creator = ActivityPub::FetchRemoteKeyService.new.call(creator_uri) if creator&.public_key.blank?
  17. return if creator.nil?
  18. options_hash = hash(@json['signature'].without('type', 'id', 'signatureValue').merge('@context' => CONTEXT))
  19. document_hash = hash(@json.without('signature'))
  20. to_be_verified = options_hash + document_hash
  21. creator if creator.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), Base64.decode64(signature), to_be_verified)
  22. rescue OpenSSL::PKey::RSAError
  23. false
  24. end
  25. def sign!(creator, sign_with: nil)
  26. options = {
  27. 'type' => 'RsaSignature2017',
  28. 'creator' => ActivityPub::TagManager.instance.key_uri_for(creator),
  29. 'created' => Time.now.utc.iso8601,
  30. }
  31. options_hash = hash(options.without('type', 'id', 'signatureValue').merge('@context' => CONTEXT))
  32. document_hash = hash(@json.without('signature'))
  33. to_be_signed = options_hash + document_hash
  34. keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : creator.keypair
  35. signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), to_be_signed))
  36. # Mastodon's context is either an array or a single URL
  37. context_with_security = Array(@json['@context'])
  38. context_with_security << 'https://w3id.org/security/v1'
  39. context_with_security.uniq!
  40. context_with_security = context_with_security.first if context_with_security.size == 1
  41. @json.merge('signature' => options.merge('signatureValue' => signature), '@context' => context_with_security)
  42. end
  43. private
  44. def hash(obj)
  45. Digest::SHA256.hexdigest(canonicalize(obj))
  46. end
  47. end