signature_verification_spec.rb 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. # frozen_string_literal: true
  2. require 'rails_helper'
  3. describe 'signature verification concern' do
  4. before do
  5. stub_tests_controller
  6. # Signature checking is time-dependent, so travel to a fixed date
  7. travel_to '2023-12-20T10:00:00Z'
  8. end
  9. after { Rails.application.reload_routes! }
  10. # Include the private key so the tests can be easily adjusted and reviewed
  11. let(:actor_keypair) do
  12. OpenSSL::PKey.read(<<~PEM_TEXT)
  13. -----BEGIN RSA PRIVATE KEY-----
  14. MIIEowIBAAKCAQEAqIAYvNFGbZ5g4iiK6feSdXD4bDStFM58A7tHycYXaYtzZQpI
  15. eHXAmaXuZzXIwtrP4N0gIk8JNwZvXj2UPS+S07t0V9wNK94he01LV5EMz/GN4eNn
  16. FmDL64HIEuKLvV8TvgjbUPRD6Y5X0UpKi2ZIFLSb96Q5w0Z/k7ntpVKV52y8kz5F
  17. jr/O/0JuHryZe0yItzJh8kzFfeMf0EXzfSnaKvT7P9jhgC6uTre+jXyvVZjiHDrn
  18. qvvucdI3I7DRfXo1OqARBrLjy+TdseUAjNYJ+OuPRI1URIWQI01DCHqcohVu9+Ar
  19. +BiCjFp3ua+XMuJvrvbD61d1Fvig/9nbBRR+8QIDAQABAoIBAAgySHnFWI6gItR3
  20. fkfiqIm80cHCN3Xk1C6iiVu+3oBOZbHpW9R7vl9e/WOA/9O+LPjiSsQOegtWnVvd
  21. RRjrl7Hj20VDlZKv5Mssm6zOGAxksrcVbqwdj+fUJaNJCL0AyyseH0x/IE9T8rDC
  22. I1GH+3tB3JkhkIN/qjipdX5ab8MswEPu8IC4ViTpdBgWYY/xBcAHPw4xuL0tcwzh
  23. FBlf4DqoEVQo8GdK5GAJ2Ny0S4xbXHUURzx/R4y4CCts7niAiLGqd9jmLU1kUTMk
  24. QcXfQYK6l+unLc7wDYAz7sFEHh04M48VjWwiIZJnlCqmQbLda7uhhu8zkF1DqZTu
  25. ulWDGQECgYEA0TIAc8BQBVab979DHEEmMdgqBwxLY3OIAk0b+r50h7VBGWCDPRsC
  26. STD73fQY3lNet/7/jgSGwwAlAJ5PpMXxXiZAE3bUwPmHzgF7pvIOOLhA8O07tHSO
  27. L2mvQe6NPzjZ+6iAO2U9PkClxcvGvPx2OBvisfHqZLmxC9PIVxzruQECgYEAzjM6
  28. BTUXa6T/qHvLFbN699BXsUOGmHBGaLRapFDBfVvgZrwqYQcZpBBhesLdGTGSqwE7
  29. gWsITPIJ+Ldo+38oGYyVys+w/V67q6ud7hgSDTW3hSvm+GboCjk6gzxlt9hQ0t9X
  30. 8vfDOYhEXvVUJNv3mYO60ENqQhILO4bQ0zi+VfECgYBb/nUccfG+pzunU0Cb6Dp3
  31. qOuydcGhVmj1OhuXxLFSDG84Tazo7juvHA9mp7VX76mzmDuhpHPuxN2AzB2SBEoE
  32. cSW0aYld413JRfWukLuYTc6hJHIhBTCRwRQFFnae2s1hUdQySm8INT2xIc+fxBXo
  33. zrp+Ljg5Wz90SAnN5TX0AQKBgDaatDOq0o/r+tPYLHiLtfWoE4Dau+rkWJDjqdk3
  34. lXWn/e3WyHY3Vh/vQpEqxzgju45TXjmwaVtPATr+/usSykCxzP0PMPR3wMT+Rm1F
  35. rIoY/odij+CaB7qlWwxj0x/zRbwB7x1lZSp4HnrzBpxYL+JUUwVRxPLIKndSBTza
  36. GvVRAoGBAIVBcNcRQYF4fvZjDKAb4fdBsEuHmycqtRCsnkGOz6ebbEQznSaZ0tZE
  37. +JuouZaGjyp8uPjNGD5D7mIGbyoZ3KyG4mTXNxDAGBso1hrNDKGBOrGaPhZx8LgO
  38. 4VXJ+ybXrATf4jr8ccZYsZdFpOphPzz+j55Mqg5vac5P1XjmsGTb
  39. -----END RSA PRIVATE KEY-----
  40. PEM_TEXT
  41. end
  42. context 'without a Signature header' do
  43. it 'does not treat the request as signed' do
  44. get '/activitypub/success'
  45. expect(response).to have_http_status(200)
  46. expect(body_as_json).to match(
  47. signed_request: false,
  48. signature_actor_id: nil,
  49. error: 'Request not signed'
  50. )
  51. end
  52. context 'when a signature is required' do
  53. it 'returns http unauthorized with appropriate error' do
  54. get '/activitypub/signature_required'
  55. expect(response).to have_http_status(401)
  56. expect(body_as_json).to match(
  57. error: 'Request not signed'
  58. )
  59. end
  60. end
  61. end
  62. context 'with an HTTP Signature from a known account' do
  63. let!(:actor) { Fabricate(:account, domain: 'remote.domain', uri: 'https://remote.domain/users/bob', private_key: nil, public_key: actor_keypair.public_key.to_pem) }
  64. context 'with a valid signature on a GET request' do
  65. let(:signature_header) do
  66. 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="' # rubocop:disable Layout/LineLength
  67. end
  68. it 'successfuly verifies signature', :aggregate_failures do
  69. expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success', { 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Host' => 'www.example.com' })
  70. get '/activitypub/success', headers: {
  71. 'Host' => 'www.example.com',
  72. 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT',
  73. 'Signature' => signature_header,
  74. }
  75. expect(response).to have_http_status(200)
  76. expect(body_as_json).to match(
  77. signed_request: true,
  78. signature_actor_id: actor.id.to_s
  79. )
  80. end
  81. end
  82. context 'with a valid signature on a GET request that has a query string' do
  83. let(:signature_header) do
  84. 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="SDMa4r/DQYMXYxVgYO2yEqGWWUXugKjVuz0I8dniQAk+aunzBaF2aPu+4grBfawAshlx1Xytl8lhb0H2MllEz16/tKY7rUrb70MK0w8ohXgpb0qs3YvQgdj4X24L1x2MnkFfKHR/J+7TBlnivq0HZqXm8EIkPWLv+eQxu8fbowLwHIVvRd/3t6FzvcfsE0UZKkoMEX02542MhwSif6cu7Ec/clsY9qgKahb9JVGOGS1op9Lvg/9y1mc8KCgD83U5IxVygYeYXaVQ6gixA9NgZiTCwEWzHM5ELm7w5hpdLFYxYOHg/3G3fiqJzpzNQAcCD4S4JxfE7hMI0IzVlNLT6A=="' # rubocop:disable Layout/LineLength
  85. end
  86. it 'successfuly verifies signature', :aggregate_failures do
  87. expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success?foo=42', { 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Host' => 'www.example.com' })
  88. get '/activitypub/success?foo=42', headers: {
  89. 'Host' => 'www.example.com',
  90. 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT',
  91. 'Signature' => signature_header,
  92. }
  93. expect(response).to have_http_status(200)
  94. expect(body_as_json).to match(
  95. signed_request: true,
  96. signature_actor_id: actor.id.to_s
  97. )
  98. end
  99. end
  100. context 'when the query string is missing from the signature verification (compatibility quirk)' do
  101. let(:signature_header) do
  102. 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="' # rubocop:disable Layout/LineLength
  103. end
  104. it 'successfuly verifies signature', :aggregate_failures do
  105. expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success', { 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Host' => 'www.example.com' })
  106. get '/activitypub/success?foo=42', headers: {
  107. 'Host' => 'www.example.com',
  108. 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT',
  109. 'Signature' => signature_header,
  110. }
  111. expect(response).to have_http_status(200)
  112. expect(body_as_json).to match(
  113. signed_request: true,
  114. signature_actor_id: actor.id.to_s
  115. )
  116. end
  117. end
  118. context 'with mismatching query string' do
  119. let(:signature_header) do
  120. 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="SDMa4r/DQYMXYxVgYO2yEqGWWUXugKjVuz0I8dniQAk+aunzBaF2aPu+4grBfawAshlx1Xytl8lhb0H2MllEz16/tKY7rUrb70MK0w8ohXgpb0qs3YvQgdj4X24L1x2MnkFfKHR/J+7TBlnivq0HZqXm8EIkPWLv+eQxu8fbowLwHIVvRd/3t6FzvcfsE0UZKkoMEX02542MhwSif6cu7Ec/clsY9qgKahb9JVGOGS1op9Lvg/9y1mc8KCgD83U5IxVygYeYXaVQ6gixA9NgZiTCwEWzHM5ELm7w5hpdLFYxYOHg/3G3fiqJzpzNQAcCD4S4JxfE7hMI0IzVlNLT6A=="' # rubocop:disable Layout/LineLength
  121. end
  122. it 'fails to verify signature', :aggregate_failures do
  123. expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success?foo=42', { 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Host' => 'www.example.com' })
  124. get '/activitypub/success?foo=43', headers: {
  125. 'Host' => 'www.example.com',
  126. 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT',
  127. 'Signature' => signature_header,
  128. }
  129. expect(body_as_json).to match(
  130. signed_request: true,
  131. signature_actor_id: nil,
  132. error: anything
  133. )
  134. end
  135. end
  136. context 'with a mismatching path' do
  137. it 'fails to verify signature', :aggregate_failures do
  138. get '/activitypub/alternative-path', headers: {
  139. 'Host' => 'www.example.com',
  140. 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT',
  141. 'Signature' => 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="', # rubocop:disable Layout/LineLength
  142. }
  143. expect(body_as_json).to match(
  144. signed_request: true,
  145. signature_actor_id: nil,
  146. error: anything
  147. )
  148. end
  149. end
  150. context 'with a mismatching method' do
  151. it 'fails to verify signature', :aggregate_failures do
  152. post '/activitypub/success', headers: {
  153. 'Host' => 'www.example.com',
  154. 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT',
  155. 'Signature' => 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="', # rubocop:disable Layout/LineLength
  156. }
  157. expect(body_as_json).to match(
  158. signed_request: true,
  159. signature_actor_id: nil,
  160. error: anything
  161. )
  162. end
  163. end
  164. context 'with an unparsable date' do
  165. let(:signature_header) do
  166. 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="d4B7nfx8RJcfdJDu1J//5WzPzK/hgtPkdzZx49lu5QhnE7qdV3lgyVimmhCFrO16bwvzIp9iRMyRLkNFxLiEeVaa1gqeKbldGSnU0B0OMjx7rFBa65vLuzWQOATDitVGiBEYqoK4v0DMuFCz2DtFaA/DIUZ3sty8bZ/Ea3U1nByLOO6MacARA3zhMSI0GNxGqsSmZmG0hPLavB3jIXoE3IDoQabMnC39jrlcO/a8h1iaxBm2WD8TejrImJullgqlJIFpKhIHI3ipQkvTGPlm9dx0y+beM06qBvWaWQcmT09eRIUefVsOAzIhUtS/7FVb/URhZvircIJDa7vtiFcmZQ=="' # rubocop:disable Layout/LineLength
  167. end
  168. it 'fails to verify signature', :aggregate_failures do
  169. expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success', { 'Date' => 'wrong date', 'Host' => 'www.example.com' })
  170. get '/activitypub/success', headers: {
  171. 'Host' => 'www.example.com',
  172. 'Date' => 'wrong date',
  173. 'Signature' => signature_header,
  174. }
  175. expect(body_as_json).to match(
  176. signed_request: true,
  177. signature_actor_id: nil,
  178. error: 'Invalid Date header: not RFC 2616 compliant date: "wrong date"'
  179. )
  180. end
  181. end
  182. context 'with a request older than a day' do
  183. let(:signature_header) do
  184. 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="G1NuJv4zgoZ3B/ZIjzDWZHK4RC+5pYee74q8/LJEMCWXhcnAomcb9YHaqk1QYfQvcBUIXw3UZ3Q9xO8F9y0i8G5mzJHfQ+OgHqCoJk8EmGwsUXJMh5s1S5YFCRt8TT12TmJZz0VMqLq85ubueSYBM7QtUE/FzFIVLvz4RysgXxaXQKzdnM6+gbUEEKdCURpXdQt2NXQhp4MAmZH3+0lQoR6VxdsK0hx0Ji2PNp1nuqFTlYqNWZazVdLBN+9rETLRmvGXknvg9jOxTTppBVWnkAIl26HtLS3wwFVvz4pJzi9OQDOvLziehVyLNbU61hky+oJ215e2HuKSe2hxHNl1MA=="' # rubocop:disable Layout/LineLength
  185. end
  186. it 'fails to verify signature', :aggregate_failures do
  187. expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success', { 'Date' => 'Wed, 18 Dec 2023 10:00:00 GMT', 'Host' => 'www.example.com' })
  188. get '/activitypub/success', headers: {
  189. 'Host' => 'www.example.com',
  190. 'Date' => 'Wed, 18 Dec 2023 10:00:00 GMT',
  191. 'Signature' => signature_header,
  192. }
  193. expect(body_as_json).to match(
  194. signed_request: true,
  195. signature_actor_id: nil,
  196. error: 'Signed request date outside acceptable time window'
  197. )
  198. end
  199. end
  200. context 'with a valid signature on a POST request' do
  201. let(:digest_header) { 'SHA-256=ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=' }
  202. let(:signature_header) do
  203. 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="host date digest (request-target)",signature="gmhMjgMROGElJU3fpehV2acD5kMHeELi8EFP2UPHOdQ54H0r55AxIpji+J3lPe+N2qSb/4H1KXIh6f0lRu8TGSsu12OQmg5hiO8VA9flcA/mh9Lpk+qwlQZIPRqKP9xUEfqD+Z7ti5wPzDKrWAUK/7FIqWgcT/mlqB1R1MGkpMFc/q4CIs2OSNiWgA4K+Kp21oQxzC2kUuYob04gAZ7cyE/FTia5t08uv6lVYFdRsn4XNPn1MsHgFBwBMRG79ng3SyhoG4PrqBEi5q2IdLq3zfre/M6He3wlCpyO2VJNdGVoTIzeZ0Zz8jUscPV3XtWUchpGclLGSaKaq/JyNZeiYQ=="' # rubocop:disable Layout/LineLength
  204. end
  205. it 'successfuly verifies signature', :aggregate_failures do
  206. expect(digest_header).to eq digest_value('Hello world')
  207. expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'post /activitypub/success', { 'Host' => 'www.example.com', 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Digest' => digest_header })
  208. post '/activitypub/success', params: 'Hello world', headers: {
  209. 'Host' => 'www.example.com',
  210. 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT',
  211. 'Digest' => digest_header,
  212. 'Signature' => signature_header,
  213. }
  214. expect(response).to have_http_status(200)
  215. expect(body_as_json).to match(
  216. signed_request: true,
  217. signature_actor_id: actor.id.to_s
  218. )
  219. end
  220. end
  221. context 'when the Digest of a POST request is not signed' do
  222. let(:digest_header) { 'SHA-256=ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=' }
  223. let(:signature_header) do
  224. 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="host date (request-target)",signature="CPD704CG8aCm8X8qIP8kkkiGp1qwFLk/wMVQHOGP0Txxan8c2DZtg/KK7eN8RG8tHx8br/yS2hJs51x4kXImYukGzNJd7ihE3T8lp+9RI1tCcdobTzr/VcVJHDFySdQkg266GCMijRQRZfNvqlJLiisr817PI+gNVBI5qV+vnVd1XhWCEZ+YSmMe8UqYARXAYNqMykTheojqGpTeTFGPUpTQA2Fmt2BipwIjcFDm2Hpihl2kB0MUS0x3zPmHDuadvzoBbN6m3usPDLgYrpALlh+wDs1dYMntcwdwawRKY1oE1XNtgOSum12wntDq3uYL4gya2iPdcw3c929b4koUzw=="' # rubocop:disable Layout/LineLength
  225. end
  226. it 'fails to verify signature', :aggregate_failures do
  227. expect(digest_header).to eq digest_value('Hello world')
  228. expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'post /activitypub/success', { 'Host' => 'www.example.com', 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT' })
  229. post '/activitypub/success', params: 'Hello world', headers: {
  230. 'Host' => 'www.example.com',
  231. 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT',
  232. 'Digest' => digest_header,
  233. 'Signature' => signature_header,
  234. }
  235. expect(body_as_json).to match(
  236. signed_request: true,
  237. signature_actor_id: nil,
  238. error: 'Mastodon requires the Digest header to be signed when doing a POST request'
  239. )
  240. end
  241. end
  242. context 'with a tampered body on a POST request' do
  243. let(:digest_header) { 'SHA-256=ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=' }
  244. let(:signature_header) do
  245. 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="host date digest (request-target)",signature="gmhMjgMROGElJU3fpehV2acD5kMHeELi8EFP2UPHOdQ54H0r55AxIpji+J3lPe+N2qSb/4H1KXIh6f0lRu8TGSsu12OQmg5hiO8VA9flcA/mh9Lpk+qwlQZIPRqKP9xUEfqD+Z7ti5wPzDKrWAUK/7FIqWgcT/mlqB1R1MGkpMFc/q4CIs2OSNiWgA4K+Kp21oQxzC2kUuYob04gAZ7cyE/FTia5t08uv6lVYFdRsn4XNPn1MsHgFBwBMRG79ng3SyhoG4PrqBEi5q2IdLq3zfre/M6He3wlCpyO2VJNdGVoTIzeZ0Zz8jUscPV3XtWUchpGclLGSaKaq/JyNZeiYQ=="' # rubocop:disable Layout/LineLength
  246. end
  247. it 'fails to verify signature', :aggregate_failures do
  248. expect(digest_header).to_not eq digest_value('Hello world!')
  249. expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'post /activitypub/success', { 'Host' => 'www.example.com', 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Digest' => digest_header })
  250. post '/activitypub/success', params: 'Hello world!', headers: {
  251. 'Host' => 'www.example.com',
  252. 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT',
  253. 'Digest' => 'SHA-256=ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=',
  254. 'Signature' => signature_header,
  255. }
  256. expect(body_as_json).to match(
  257. signed_request: true,
  258. signature_actor_id: nil,
  259. error: 'Invalid Digest value. Computed SHA-256 digest: wFNeS+K3n/2TKRMFQ2v4iTFOSj+uwF7P/Lt98xrZ5Ro=; given: ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw='
  260. )
  261. end
  262. end
  263. context 'with a tampered path in a POST request' do
  264. it 'fails to verify signature', :aggregate_failures do
  265. post '/activitypub/alternative-path', params: 'Hello world', headers: {
  266. 'Host' => 'www.example.com',
  267. 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT',
  268. 'Digest' => 'SHA-256=ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=',
  269. 'Signature' => 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="host date digest (request-target)",signature="gmhMjgMROGElJU3fpehV2acD5kMHeELi8EFP2UPHOdQ54H0r55AxIpji+J3lPe+N2qSb/4H1KXIh6f0lRu8TGSsu12OQmg5hiO8VA9flcA/mh9Lpk+qwlQZIPRqKP9xUEfqD+Z7ti5wPzDKrWAUK/7FIqWgcT/mlqB1R1MGkpMFc/q4CIs2OSNiWgA4K+Kp21oQxzC2kUuYob04gAZ7cyE/FTia5t08uv6lVYFdRsn4XNPn1MsHgFBwBMRG79ng3SyhoG4PrqBEi5q2IdLq3zfre/M6He3wlCpyO2VJNdGVoTIzeZ0Zz8jUscPV3XtWUchpGclLGSaKaq/JyNZeiYQ=="', # rubocop:disable Layout/LineLength
  270. }
  271. expect(response).to have_http_status(200)
  272. expect(body_as_json).to match(
  273. signed_request: true,
  274. signature_actor_id: nil,
  275. error: anything
  276. )
  277. end
  278. end
  279. end
  280. context 'with an inaccessible key' do
  281. before do
  282. stub_request(:get, 'https://remote.domain/users/alice#main-key').to_return(status: 404)
  283. end
  284. it 'fails to verify signature', :aggregate_failures do
  285. get '/activitypub/success', headers: {
  286. 'Host' => 'www.example.com',
  287. 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT',
  288. 'Signature' => 'keyId="https://remote.domain/users/alice#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="', # rubocop:disable Layout/LineLength
  289. }
  290. expect(body_as_json).to match(
  291. signed_request: true,
  292. signature_actor_id: nil,
  293. error: 'Unable to fetch key JSON at https://remote.domain/users/alice#main-key'
  294. )
  295. end
  296. end
  297. private
  298. def stub_tests_controller
  299. stub_const('ActivityPub::TestsController', activitypub_tests_controller)
  300. Rails.application.routes.draw do
  301. # NOTE: RouteSet#draw removes all routes, so we need to re-insert one
  302. resource :instance_actor, path: 'actor', only: [:show]
  303. match :via => [:get, :post], '/activitypub/success' => 'activitypub/tests#success'
  304. match :via => [:get, :post], '/activitypub/alternative-path' => 'activitypub/tests#alternative_success'
  305. match :via => [:get, :post], '/activitypub/signature_required' => 'activitypub/tests#signature_required'
  306. end
  307. end
  308. def activitypub_tests_controller
  309. Class.new(ApplicationController) do
  310. include SignatureVerification
  311. before_action :require_actor_signature!, only: [:signature_required]
  312. def success
  313. render json: {
  314. signed_request: signed_request?,
  315. signature_actor_id: signed_request_actor&.id&.to_s,
  316. }.merge(signature_verification_failure_reason || {})
  317. end
  318. alias_method :alternative_success, :success
  319. alias_method :signature_required, :success
  320. end
  321. end
  322. def digest_value(body)
  323. "SHA-256=#{Digest::SHA256.base64digest(body)}"
  324. end
  325. def build_signature_string(keypair, key_id, request_target, headers)
  326. algorithm = 'rsa-sha256'
  327. signed_headers = headers.merge({ '(request-target)' => request_target })
  328. signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n")
  329. signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))
  330. "keyId=\"#{key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\""
  331. end
  332. end