t_http.ml 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836
  1. (*
  2. * _ _ ____ _
  3. * _| || |_/ ___| ___ _ __ _ __ ___ | |
  4. * |_ .. _\___ \ / _ \ '_ \| '_ \ / _ \| |
  5. * |_ _|___) | __/ |_) | |_) | (_) |_|
  6. * |_||_| |____/ \___| .__/| .__/ \___/(_)
  7. * |_| |_|
  8. *
  9. * Personal Social Web.
  10. *
  11. * http_test.ml
  12. *
  13. * Copyright (C) The #Seppo contributors. All rights reserved.
  14. *
  15. * This program is free software: you can redistribute it and/or modify
  16. * it under the terms of the GNU General Public License as published by
  17. * the Free Software Foundation, either version 3 of the License, or
  18. * (at your option) any later version.
  19. *
  20. * This program is distributed in the hope that it will be useful,
  21. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  22. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  23. * GNU General Public License for more details.
  24. *
  25. * You should have received a copy of the GNU General Public License
  26. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  27. *)
  28. open Alcotest
  29. open Seppo_lib
  30. let set_up () =
  31. Mirage_crypto_rng_lwt.initialize (module Mirage_crypto_rng.Fortuna);
  32. Unix.chdir "../../../test/"
  33. let tc_relpa () =
  34. Http.relpa "a/b/" "a/b/d/e" |> Assrt.equals_string __LOC__ "d/e";
  35. Http.relpa "a/B/" "a/b/d/e" |> Assrt.equals_string __LOC__ "";
  36. let base = Uri.of_string "a/b/" in
  37. Uri.resolve "" base (Uri.of_string "c/d/") |> Uri.to_string |> check string __LOC__ "a/b/c/d/";
  38. Uri.resolve "" base ((Http.relpa (base |> Uri.to_string) ("a/b/c/d/")) |> Uri.of_string) |> Uri.to_string |> check string __LOC__ "a/b/c/d/";
  39. ()
  40. let tc_uri () =
  41. let base = "https://example.com:443/a/b/c?d=e#f" |> Uri.of_string in
  42. base |> Uri.path
  43. |> Assrt.equals_string __LOC__ "/a/b/c";
  44. "../i.j" |> Uri.of_string |> Http.reso ~base |> Uri.to_string
  45. |> Assrt.equals_string __LOC__ "https://example.com:443/a/i.j";
  46. let re = "https://example.com:443/a/b/C/d.e#ff" |> Uri.of_string |> Http.abs_to_rel ~base in
  47. re |> Uri.to_string |> Assrt.equals_string __LOC__ "C/d.e#ff";
  48. Uri.make ~path:"." () |> Http.reso ~base:Uri.empty |> Uri.to_string |> Assrt.equals_string __LOC__ "";
  49. let a = Uri.of_string "https://example.com/path" in
  50. assert ("https://example.com/path" |> Uri.of_string |> Uri.equal a);
  51. assert ("https://EXAMPLE.com/path" |> Uri.of_string |> Uri.equal a);
  52. ()
  53. let tc_rel_cd () =
  54. "/seppo.cgi" |> Cgi.cd_cgi_bin_twin_path |> Assrt.equals_string __LOC__ ".";
  55. "/sub/dir/seppo.cgi" |> Cgi.cd_cgi_bin_twin_path |> Assrt.equals_string __LOC__ ".";
  56. "/cgi-bin/seppo.cgi" |> Cgi.cd_cgi_bin_twin_path |> Assrt.equals_string __LOC__ "..";
  57. "/cgi-bin/sub/dir/seppo.cgi" |> Cgi. cd_cgi_bin_twin_path |> Assrt.equals_string __LOC__ "../../../sub/dir"
  58. module Request = struct
  59. let tc_rx_script_name () =
  60. let chk n fn =
  61. if Str.string_match Cgi.Request.rx_script_name fn 0
  62. then (
  63. fn |> Str.matched_group 5 |> Assrt.equals_string __LOC__ n;
  64. fn |> Str.matched_group 3 )
  65. else "no match"
  66. in
  67. "/seppo.cgi" |> chk "seppo.cgi" |> Assrt.equals_string __LOC__ "/";
  68. "/cgi-bin/seppo.cgi" |> chk "seppo.cgi" |> Assrt.equals_string __LOC__ "/";
  69. "/cgi-bin/uhu/seppo.cgi" |> chk "seppo.cgi" |> Assrt.equals_string __LOC__ "/uhu/";
  70. "/apchk.cgi" |> chk "apchk.cgi" |> Assrt.equals_string __LOC__ "/";
  71. "/cgi-bin/uhu/apchk.cgi" |> chk "apchk.cgi" |> Assrt.equals_string __LOC__ "/uhu/";
  72. ()
  73. let tc_uri () =
  74. let r : Cgi.Request.t = {
  75. content_type = "text/plain";
  76. content_length = None;
  77. host = "example.com";
  78. http_cookie = "";
  79. path_info = "/shaarli";
  80. query_string = "post=uhu";
  81. request_method = "GET";
  82. remote_addr = "127.0.0.1";
  83. scheme = "https";
  84. script_name = "/sub/seppo.cgi";
  85. server_port = "443";
  86. raw_string = Sys.getenv_opt
  87. } in
  88. r |> Cgi.Request.abs |> Uri.to_string |> Assrt.equals_string __LOC__ "https://example.com/sub/seppo.cgi/shaarli?post=uhu";
  89. r |> Cgi.Request.path_and_query |> Uri.to_string |> Assrt.equals_string __LOC__ "/sub/seppo.cgi/shaarli?post=uhu";
  90. "a" |> Assrt.equals_string __LOC__ "a";
  91. assert true
  92. let tc_base () =
  93. Uri.make ~scheme:"https" ~host:"example.com" ()
  94. |> Cgi.Request.base' "/seppo.cgi"
  95. |> Uri.to_string
  96. |> Assrt.equals_string __LOC__ "https://example.com/";
  97. Uri.make ~scheme:"https" ~host:"example.com" ()
  98. |> Cgi.Request.base' "/a/b/seppo.cgi"
  99. |> Uri.to_string
  100. |> Assrt.equals_string __LOC__ "https://example.com/a/b/";
  101. let r : Cgi.Request.t = {
  102. content_type = "text/plain";
  103. content_length = None;
  104. host = "example.com";
  105. http_cookie = "";
  106. path_info = "/shaarli";
  107. query_string = "post=uhu";
  108. request_method = "GET";
  109. remote_addr = "127.0.0.1";
  110. scheme = "https";
  111. script_name = "/cgi-bin/sub/seppo.cgi";
  112. server_port = "443";
  113. raw_string = Sys.getenv_opt
  114. } in
  115. r |> Cgi.Request.base
  116. |> Uri.to_string
  117. |> Assrt.equals_string __LOC__ "https://example.com/sub/";
  118. {r with script_name = "/sib/seppo.cgi"}
  119. |> Cgi.Request.base
  120. |> Uri.to_string
  121. |> Assrt.equals_string __LOC__ "https://example.com/sib/";
  122. {r with script_name = "/seppo.cgi"}
  123. |> Cgi.Request.base
  124. |> Uri.to_string
  125. |> Assrt.equals_string __LOC__ "https://example.com/";
  126. ()
  127. let tc_query_string () =
  128. match "" |> Uri.query_of_encoded with
  129. | [("",[])] -> ()
  130. | _ -> "no" |> Assrt.equals_string __LOC__ ""
  131. end
  132. module Cookie = struct
  133. let tc_rfc1123 () =
  134. let s = "Thu, 01 Jan 1970 00:00:00 GMT" in
  135. Ptime.epoch |> Http.to_rfc1123 |> Assrt.equals_string __LOC__ s;
  136. assert true
  137. let tc_to_string () =
  138. let http_only = Some true
  139. and path = Some "seppo.cgi"
  140. and same_site = Some `Strict
  141. and max_age = Some (30. *. 60.)
  142. and secure = Some true in
  143. Cookie.to_string ?path ?secure ?http_only ?same_site ("auth_until", "2022-04-08T22:30:07Z")
  144. |> Assrt.equals_string __LOC__
  145. "auth_until=2022-04-08T22:30:07Z; Path=seppo.cgi; Secure; HttpOnly; \
  146. SameSite=Strict";
  147. Cookie.to_string ?max_age ?path ?secure ?http_only ?same_site ("auth", "yes")
  148. |> Assrt.equals_string __LOC__
  149. "auth=yes; Max-Age=1800; Path=seppo.cgi; Secure; HttpOnly; \
  150. SameSite=Strict";
  151. assert true
  152. let tc_of_string () =
  153. let c = Cookie.to_string ("#Seppo!", "foo") in
  154. c |> Assrt.equals_string __LOC__ "#Seppo!=foo";
  155. let v = match c |> Cookie.of_string with
  156. | ("#Seppo!", v) :: [] -> v
  157. | _ -> assert false
  158. in
  159. v |> Assrt.equals_string __LOC__ "foo";
  160. assert true
  161. end
  162. module Header = struct
  163. let tc_headers () =
  164. Logr.info (fun m -> m "http_test.test_headers");
  165. let h = [ ("A", "a"); ("B", "b") ] @ [ ("C", "c") ]
  166. |> Cohttp.Header.of_list in
  167. h |> Cohttp.Header.to_string
  168. |> Assrt.equals_string __LOC__ "A: a\r\nB: b\r\nC: c\r\n\r\n";
  169. h |> Cohttp.Header.to_frames
  170. |> String.concat "\n"
  171. |> Assrt.equals_string __LOC__ "A: a\nB: b\nC: c";
  172. Cohttp.Header.get h "a"
  173. |> Option.value ~default:"-"
  174. |> Assrt.equals_string __LOC__ "a";
  175. assert true
  176. let tc_sig_encode () =
  177. [ "k1","v1";
  178. "k2","v2"; ] |> Http.Signature.encode
  179. |> check string __LOC__ {|k1="v1",k2="v2"|};
  180. `GET
  181. |> Cohttp.Code.string_of_method
  182. |> Astring.String.map Astring.Char.Ascii.lowercase
  183. |> check string __LOC__ "get"
  184. let tc_signature () =
  185. Logr.info (fun m -> m "http_test.test_signature");
  186. let si = {|keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date",signature="qdx+H7PHHDZgy4y/Ahn9Tny9V3GP6YgBPyUXMmoxWtLbHpUnXS2mg2+SbrQDMCJypxBLSPQR2aAjn7ndmw2iicw3HMbe8VfEdKFYRqzic+efkb3nndiv/x1xSHDJWeSWkx3ButlYSuBskLu6kd9Fswtemr3lgdDEmn04swr2Os0="|} in
  187. let si' = si
  188. |> Http.Signature.decode
  189. |> Result.get_ok in
  190. si' |> List.length |> Assrt.equals_int __LOC__ 4;
  191. si'
  192. |> Tyre.eval Http.Signature.P.list_auth_param
  193. |> check string __LOC__ si
  194. let priv_key_cavage =
  195. {|-----BEGIN RSA PRIVATE KEY-----
  196. MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF
  197. NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F
  198. UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB
  199. AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA
  200. QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK
  201. kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg
  202. f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u
  203. 412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc
  204. mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7
  205. kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA
  206. gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW
  207. G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI
  208. 7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA==
  209. -----END RSA PRIVATE KEY-----
  210. |}
  211. |> Cstruct.of_string
  212. |> X509.Private_key.decode_pem |> Result.get_ok
  213. let pub_key_cavage =
  214. {|-----BEGIN PUBLIC KEY-----
  215. MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3
  216. 6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6
  217. Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw
  218. oYi+1hqp1fIekaxsyQIDAQAB
  219. -----END PUBLIC KEY-----
  220. |}
  221. |> Cstruct.of_string
  222. |> X509.Public_key.decode_pem |> Result.get_ok
  223. let tc_sign2 () =
  224. `GET |> Cohttp.Code.string_of_method |> check string __LOC__ "GET";
  225. let n,s = ["date","today";
  226. "digest","SHA256=0815"]
  227. |> Http.Signature.to_sign_string
  228. `GET
  229. ("https://example.com/uhu?foo=bar#baz" |> Uri.of_string) in
  230. n |> check string __LOC__ "(request-target) host date digest";
  231. s |> check string __LOC__ "(request-target): get /uhu?foo=bar\nhost: example.com\ndate: today\ndigest: SHA256=0815";
  232. ["host","example.com";
  233. "date","today";
  234. "digest","SHA256=0815"]
  235. |> Http.Signature.add
  236. priv_key_cavage
  237. `GET
  238. ("https://example.com/uhu?foo=bar#baz" |> Uri.of_string)
  239. |> Result.get_ok
  240. |> Cohttp.Header.of_list
  241. |> Cohttp.Header.to_frames |> Astring.String.concat ~sep:"\n"
  242. |> check string __LOC__ {|host: example.com
  243. date: today
  244. digest: SHA256=0815
  245. signature: algorithm="rsa-sha256",headers="(request-target) host date digest",signature="AFq6XChsi63zuCVVzeVigx7BV/HzHnsg304i9uqJ44t2QufQ4WvYS1jDh2B539B3VyBQiuXoiNrSssMoShVORmZzA1y4dnnFlYncFdQRsRDRA//E2YB39ECSby0Fl6pBK+Ws/090RWcxFxTBFsD0H9JQuVASbBCDxy2lhHTFugg="|}
  246. let tc_to_sign_string_basic () =
  247. let open Cohttp in
  248. let uri = Uri.of_string "/foo?param=value&pet=dog" in
  249. [
  250. "host", "example.com" ;
  251. "date", "Sun, 05 Jan 2014 21:31:40 GMT" ;
  252. ]
  253. |> Header.of_list
  254. |> Http.Signature.to_sign_string0 ~request:(Some (`POST,uri))
  255. |> Assrt.equals_string __LOC__
  256. {|(request-target): post /foo?param=value&pet=dog
  257. host: example.com
  258. date: Sun, 05 Jan 2014 21:31:40 GMT|};
  259. assert true
  260. (*
  261. * https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#appendix-C.2
  262. *)
  263. let tc_sign_basic () =
  264. Logr.info (fun m -> m "http_test.test_sign_basic");
  265. let pk = priv_key_cavage in
  266. let open Cohttp in
  267. let sig_ = "qdx+H7PHHDZgy4y/Ahn9Tny9V3GP6YgBPyUXMmoxWtLbHpUnXS2mg2+SbrQDMCJypxBLSPQR2aAjn7ndmw2iicw3HMbe8VfEdKFYRqzic+efkb3nndiv/x1xSHDJWeSWkx3ButlYSuBskLu6kd9Fswtemr3lgdDEmn04swr2Os0="
  268. and uri = Uri.of_string "/foo?param=value&pet=dog"
  269. and h = [
  270. "host", "example.com" ;
  271. "date", "Sun, 05 Jan 2014 21:31:40 GMT" ;
  272. ] |> Header.of_list in
  273. let s = h |> Http.Signature.to_sign_string0 ~request:(Some (`POST,uri)) in
  274. s |> Assrt.equals_string __LOC__
  275. "(request-target): post /foo?param=value&pet=dog\n\
  276. host: example.com\n\
  277. date: Sun, 05 Jan 2014 21:31:40 GMT";
  278. let al,si = s |> Cstruct.of_string |> Ap.PubKeyPem.sign pk in
  279. al |> Assrt.equals_string __LOC__ "rsa-sha256";
  280. si |> Cstruct.to_string |> Base64.encode_exn |> Assrt.equals_string __LOC__ sig_;
  281. Logr.info (fun m -> m "http_test.test_sign_basic II");
  282. let pub = pub_key_cavage in
  283. (match Ap.PubKeyPem.verify ~algo:"rsa-sha256" ~inbox:Uri.empty ~key:pub ~signature:si (s |> Cstruct.of_string) with
  284. | Error `Msg e -> e |> Assrt.equals_string __LOC__ ""
  285. | Ok _ -> "ha!" |> Assrt.equals_string __LOC__ "ha!");
  286. assert true
  287. (*
  288. * https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#appendix-C.3
  289. *)
  290. let tc_sign_all_headers () =
  291. Logr.info (fun m -> m "http_test.test_sign_all_headers");
  292. let open Cohttp in
  293. let h = [
  294. ("(request-target)", "post /foo?param=value&pet=dog");
  295. ("(created)", "1402170695");
  296. ("(expires)", "1402170699");
  297. ("host", "example.com");
  298. ("date", "Sun, 05 Jan 2014 21:31:40 GMT");
  299. ("content-type", "application/json");
  300. ("digest", "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=");
  301. ("content-length", "18");
  302. ] |> Header.of_list in
  303. h
  304. |> Header.to_frames
  305. |> String.concat "\n"
  306. |> Assrt.equals_string __LOC__
  307. "(request-target): post /foo?param=value&pet=dog\n\
  308. (created): 1402170695\n\
  309. (expires): 1402170699\n\
  310. host: example.com\n\
  311. date: Sun, 05 Jan 2014 21:31:40 GMT\n\
  312. content-type: application/json\n\
  313. digest: SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=\n\
  314. content-length: 18"
  315. ;
  316. let pk = priv_key_cavage in
  317. let al,si = h
  318. |> Header.to_frames
  319. |> String.concat "\n"
  320. |> Cstruct.of_string
  321. |> Ap.PubKeyPem.sign pk
  322. in
  323. (* |> Assrt.equals_string __LOC__
  324. "vSdrb+dS3EceC9bcwHSo4MlyKS59iFIrhgYkz8+oVLEEzmYZZvRs8rgOp+63LEM3v+MFHB32NfpB2bEKBIvB1q52LaEUHFv120V01IL+TAD48XaERZFukWgHoBTLMhYS2Gb51gWxpeIq8knRmPnYePbF5MOkR0Zkly4zKH7s1dE="
  325. *)
  326. al |> Assrt.equals_string __LOC__ "rsa-sha256";
  327. si |> Cstruct.to_string |> Base64.encode_exn |> Assrt.equals_string __LOC__
  328. "nAkCW0wg9AbbStQRLi8fsS1mPPnA6S5+/0alANcoDFG9hG0bJ8NnMRcB1Sz1eccNMzzLEke7nGXqoiJYZFfT81oaRqh/MNFwQVX4OZvTLZ5xVZQuchRkOSO7b2QX0aFWFOUq6dnwAyliHrp6w3FOxwkGGJPaerw2lOYLdC/Bejk="
  329. let tc_signed_headers () =
  330. Logr.info (fun m -> m "http_test.test_signed_headers");
  331. let open Cohttp in
  332. (* values from
  333. https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#appendix-C.3
  334. *)
  335. let id = Uri.of_string "https://example.com/actor/"
  336. and dgst = Some "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE="
  337. and date,_,_ = Ptime.of_rfc3339 "2014-01-05T22:31:40+01:00" |> Result.get_ok
  338. and uri = Uri.of_string "https://example.com/foo?param=value&pet=dog" in
  339. let key_id = Uri.with_fragment id (Some "main-key")
  340. and pk = priv_key_cavage in
  341. Http.signed_headers (key_id,pk,date) dgst uri
  342. |> Header.to_frames
  343. |> String.concat "\n"
  344. |> Assrt.equals_string __LOC__
  345. "host: example.com\n\
  346. date: Sun, 05 Jan 2014 21:31:40 GMT\n\
  347. digest: SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=\n\
  348. signature: \
  349. keyId=\"https://example.com/actor/#main-key\",\
  350. algorithm=\"rsa-sha256\",\
  351. headers=\"(request-target) host date digest\",\
  352. signature=\"WC34OEWXgO0viIZAu5qnBcKj5nOMlgjs0ASxgJPYX9x1VtKrYRRhAosH7ixFnkJneSHGn8yY9lowNvbdBg+ZsINx6P0e1WyB0YJbwsREYKYpG1sjwS3R3iCXmXf3m+txiCNhFcbbvb0Grq3wbAWGB0VW7ymI6AHixDXFLD5IYl4=\""
  353. (* https://datatracker.ietf.org/doc/html/rfc7235#appendix-C *)
  354. let tc_parse_auth_params () =
  355. Logr.info (fun m -> m "http_test.test_parse_auth_param");
  356. let module P = Http.Signature.P in
  357. (match {|uhu|} |> Tyre.exec (P.token |> Tyre.compile) with
  358. | Ok "uhu" -> "super"
  359. | _ -> "was anderes")
  360. |> Assrt.equals_string __LOC__ "super";
  361. (match {|"uhu"|} |> Tyre.exec (P.quoted_string |> Tyre.compile) with
  362. | Ok "uhu" -> "super"
  363. | _ -> "was anderes")
  364. |> Assrt.equals_string __LOC__ "super";
  365. (match {|uhu="aha"|} |> Tyre.exec (P.auth_param|> Tyre.compile) with
  366. | Ok ("uhu","aha") -> "super"
  367. | _ -> "was anderes")
  368. |> Assrt.equals_string __LOC__ "super";
  369. (match {|uhu="ah\"a"|} |> Tyre.exec (P.auth_param|> Tyre.compile) with
  370. | Ok ("uhu",{|ah"a|}) -> "super"
  371. | _ -> "was anderes")
  372. |> Assrt.equals_string __LOC__ "super";
  373. (match {|a="A", b="B"|} |> Tyre.exec (P.list_auth_param|> Tyre.compile) with
  374. | Ok [("a","A"); ("b","B")] -> "super"
  375. | _ -> "was anderes")
  376. |> Assrt.equals_string __LOC__ "super";
  377. (match {|a="A", nasty="na,s\"ty",b="B"|} |> Tyre.exec (P.list_auth_param|> Tyre.compile) with
  378. | Ok [("a","A");
  379. ("nasty",{|na,s"ty|});
  380. ("b","B")] -> "super"
  381. | _ -> "was anderes")
  382. |> Assrt.equals_string __LOC__ "super";
  383. assert true
  384. let tc_parse_signature () =
  385. Logr.info (fun m -> m "http_test.test_parse_signature");
  386. (* https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-4.1.1 *)
  387. let _sihe = {|keyId="rsa-key-1",algorithm="hs2019", created=1402170695, expires=1402170995, headers="(request-target) (created) (expires) host date digest content-length", signature="Base64(RSA-SHA256(signing string))"|}
  388. |> Http.Signature.decode in
  389. let _sihe = {|keyId="hmac-key-1",algorithm="hs2019",created=1402170695,headers="(request-target) (created) host digest content-length",signature="Base64(HMAC-SHA512(signing string))"|}
  390. |> Http.Signature.decode in
  391. (*
  392. date='Thu, 29 Jun 2023 09:51:37 GMT' digest='SHA-256=rSBxGz18uv2ZvY9PxjkuKv6ZWR78M/5S2m+yOXrq+ik=' signature='keyId="https://alpaka.social/users/traunstein#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="JIHBg3VahvgFweniUBfH0QSHOuilcYW313i7H6gptKT/uOSfs5QhADm7LKLZ6q7jZWtQLi4Ge8dhxVeYhGpdU5P3iABn665z3TvuUiwVUO0sGI6yAv+z9wVmFfPLFsTYOB09Fy+yht+E4Z9GOF6C/U79eb/y8QOuj1OJB3L+427IQpnJMuPh5e22LBM1E/eXLbvWyshKqX0n8WZj4qPezzsH21Afn+dUnd2jc2XqUbOpzeFkz45ut0okZAF3686/sQ0sBcloSFfvdB+EuLqZLJSYcnMe3Qe8dUpibgm5+v0XfgLZYPL2P7VpuMXkQB9neRbSCdTWojcABBwUGWV0DA=="'
  393. *)
  394. let h = [
  395. ("date",{|Thu, 29 Jun 2023 09:51:37 GMT|});
  396. ("digest",{|SHA-256=rSBxGz18uv2ZvY9PxjkuKv6ZWR78M/5S2m+yOXrq+ik=|});
  397. ("signature",{|keyId="https://alpaka.social/users/traunstein#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="JIHBg3VahvgFweniUBfH0QSHOuilcYW313i7H6gptKT/uOSfs5QhADm7LKLZ6q7jZWtQLi4Ge8dhxVeYhGpdU5P3iABn665z3TvuUiwVUO0sGI6yAv+z9wVmFfPLFsTYOB09Fy+yht+E4Z9GOF6C/U79eb/y8QOuj1OJB3L+427IQpnJMuPh5e22LBM1E/eXLbvWyshKqX0n8WZj4qPezzsH21Afn+dUnd2jc2XqUbOpzeFkz45ut0okZAF3686/sQ0sBcloSFfvdB+EuLqZLJSYcnMe3Qe8dUpibgm5+v0XfgLZYPL2P7VpuMXkQB9neRbSCdTWojcABBwUGWV0DA=="|});
  398. ] |> Cohttp.Header.of_list in
  399. let sh = "signature" |> Cohttp.Header.get h |> Option.value ~default:"-" in
  400. sh
  401. |> Assrt.equals_string __LOC__ {|keyId="https://alpaka.social/users/traunstein#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="JIHBg3VahvgFweniUBfH0QSHOuilcYW313i7H6gptKT/uOSfs5QhADm7LKLZ6q7jZWtQLi4Ge8dhxVeYhGpdU5P3iABn665z3TvuUiwVUO0sGI6yAv+z9wVmFfPLFsTYOB09Fy+yht+E4Z9GOF6C/U79eb/y8QOuj1OJB3L+427IQpnJMuPh5e22LBM1E/eXLbvWyshKqX0n8WZj4qPezzsH21Afn+dUnd2jc2XqUbOpzeFkz45ut0okZAF3686/sQ0sBcloSFfvdB+EuLqZLJSYcnMe3Qe8dUpibgm5+v0XfgLZYPL2P7VpuMXkQB9neRbSCdTWojcABBwUGWV0DA=="|};
  402. (match sh |> Http.Signature.decode
  403. (* Http.Signature.decode *) with
  404. | Ok sh ->
  405. sh |> List.length |> Assrt.equals_int __LOC__ 4;
  406. List.assoc_opt "keyId" sh |> Option.value ~default:"-"
  407. |> Assrt.equals_string __LOC__ "https://alpaka.social/users/traunstein#main-key";
  408. List.assoc_opt "algorithm" sh |> Option.value ~default:"-"
  409. |> Assrt.equals_string __LOC__ "rsa-sha256";
  410. List.assoc_opt "headers" sh |> Option.value ~default:"-"
  411. |> Assrt.equals_string __LOC__ "(request-target) host date digest content-type";
  412. List.assoc_opt "signature" sh |> Option.value ~default:"-"
  413. |> Assrt.equals_string __LOC__ "JIHBg3VahvgFweniUBfH0QSHOuilcYW313i7H6gptKT/uOSfs5QhADm7LKLZ6q7jZWtQLi4Ge8dhxVeYhGpdU5P3iABn665z3TvuUiwVUO0sGI6yAv+z9wVmFfPLFsTYOB09Fy+yht+E4Z9GOF6C/U79eb/y8QOuj1OJB3L+427IQpnJMuPh5e22LBM1E/eXLbvWyshKqX0n8WZj4qPezzsH21Afn+dUnd2jc2XqUbOpzeFkz45ut0okZAF3686/sQ0sBcloSFfvdB+EuLqZLJSYcnMe3Qe8dUpibgm5+v0XfgLZYPL2P7VpuMXkQB9neRbSCdTWojcABBwUGWV0DA=="
  414. | _ -> "fail" |> Assrt.equals_string __LOC__ "");
  415. assert true
  416. let tc_verify_basic () =
  417. Logr.info (fun m -> m "http_test.test_verify_basic");
  418. let pub = pub_key_cavage in
  419. let h = [
  420. ("some", "bogus");
  421. ("date", {|Sun, 05 Jan 2014 21:31:40 GMT|});
  422. ("signature", {|keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date",signature="qdx+H7PHHDZgy4y/Ahn9Tny9V3GP6YgBPyUXMmoxWtLbHpUnXS2mg2+SbrQDMCJypxBLSPQR2aAjn7ndmw2iicw3HMbe8VfEdKFYRqzic+efkb3nndiv/x1xSHDJWeSWkx3ButlYSuBskLu6kd9Fswtemr3lgdDEmn04swr2Os0="|});
  423. ("more", "bogus");
  424. ("host", {|example.com|});
  425. ] |> Cohttp.Header.of_list in
  426. (* fetch http header values and map from lowercase plus the special name (request-target) *)
  427. let hdr = Cohttp.Header.get h in
  428. (* take a list of header names and fetch them incl. values. *)
  429. let hdrs =
  430. List.fold_left
  431. (fun init k ->
  432. (match hdr k with
  433. | None -> init
  434. | Some v -> Cohttp.Header.add init k v)
  435. )
  436. (Cohttp.Header.init ()) in
  437. let foo () =
  438. Logr.debug (fun m -> m "%s.%s get & parse the signature header" "Ap.Inbox" "post");
  439. let ( let* ) = Result.bind in
  440. let* si_v = "signature" |> hdr |> Option.to_result ~none:Http.s502' in
  441. let* si_v = si_v
  442. |> Http.Signature.decode
  443. |> Result.map_error
  444. (function
  445. | `NoMatch _
  446. | `ConverterFailure _ ->
  447. Logr.debug (fun m -> m "%s.%s Signature parsing failure" "Ap.Inbox" "post");
  448. Http.s502') in
  449. let* algo = si_v |> List.assoc_opt "algorithm" |> Option.to_result ~none:Http.s502' in
  450. let* heads = si_v |> List.assoc_opt "headers" |> Option.to_result ~none:Http.s502' in
  451. let heads = heads |> String.split_on_char ' ' in
  452. let* keyid = si_v |> List.assoc_opt "keyId" |> Option.to_result ~none:Http.s502' in
  453. let _keyid = keyid |> Uri.of_string in
  454. let* sign = si_v |> List.assoc_opt "signature" |> Option.to_result ~none:Http.s502' in
  455. let sign = sign |> Base64.decode_exn |> Cstruct.of_string in
  456. Logr.debug (fun m -> m "%s.%s fetch the remote actor profile & key" "Ap.Inbox" "post");
  457. Logr.debug (fun m -> m "%s.%s get the verified header values, signature algorithm %s" "Ap.Inbox" "post" algo);
  458. let heads = heads |> hdrs in
  459. let* _ = heads
  460. |> Http.Signature.to_sign_string0 ~request:(Some (`POST,Uri.of_string "/foo?param=value&pet=dog"))
  461. |> Cstruct.of_string
  462. |> Ap.PubKeyPem.verify ~algo ~inbox:Uri.empty ~key:pub ~signature:sign
  463. |> Result.map_error (fun (`Msg e) ->
  464. Logr.warn (fun m -> m "%s.%s %s" "Ap.Inbox" "post" e);
  465. Http.s502') in
  466. Ok heads
  467. in
  468. let v l n = Cohttp.Header.get l n |> Option.value ~default:"?" in
  469. (match foo () with
  470. | Error _ -> "aua" |> Assrt.equals_string __LOC__ "-"
  471. | Ok h->
  472. h |> Cohttp.Header.to_list |> List.length |> Assrt.equals_int __LOC__ 2;
  473. "date" |> v h |> Assrt.equals_string __LOC__ "Sun, 05 Jan 2014 21:31:40 GMT";
  474. "host" |> v h |> Assrt.equals_string __LOC__ "example.com");
  475. assert true
  476. let tc_verify_hs2019_raw () =
  477. (*
  478. 2024-09-16T14:26:45.455+02:00 DEBUG Is2s.Inbox.post ba111224-c9b7-4c56-ae48-82f04795f23e Signature: keyId="https://gotosocial.dev.seppo.social/users/demo/main-key",algorithm="hs2019",headers="(request-target) host date digest",signature="MG9tIV9rWJHDFKEFGsjakYoBtPjZbyk/ddTn6Xr2xHkmTVZDmkJmGcD4yDfWfQ4m8BYS+jd4lnb8O5fdm/pFwpFDGU70IDLsg6INGxZJQKuWbQB7dFEBJt22h8GcjOIlXvw4cKsgc3KvplIjTrFlnYiQQVvcSy+uQRXJTJTm2Y6vxOQzFvSJa0S8lXz5+x/CqpqXJtj1cSztEHZEFdBla2M30smV1uJvQcfa+lIRPwXdwtL0COsg8J00hAYBoFXPo+4N/jytArkYOFz6MasUrRODURuAE2fR6JI2aAerBy0WFE17TyWuXjWlnYt6t9aO5Wzo/qc/3DgMWHQr8NZGVg=="
  479. 2024-09-16T14:26:45.608+02:00 DEBUG Is2s.Inbox.post signature check '(request-target): post /2024-03-19/seppo.cgi/activitypub/inbox.jsa
  480. host: dev.seppo.social
  481. date: Mon, 16 Sep 2024 12:26:45 GMT
  482. digest: SHA-256=bcLAcfJg/048pSOMeA29j/PqW2iQJw96mT+egmjF+Zk='
  483. *)
  484. let h = [
  485. ("host", {|dev.seppo.social|});
  486. ("date", {|Mon, 16 Sep 2024 12:26:45 GMT|});
  487. ("signature", {|keyId="https://gotosocial.dev.seppo.social/users/demo/main-key",algorithm="hs2019",headers="(request-target) host date digest",signature="MG9tIV9rWJHDFKEFGsjakYoBtPjZbyk/ddTn6Xr2xHkmTVZDmkJmGcD4yDfWfQ4m8BYS+jd4lnb8O5fdm/pFwpFDGU70IDLsg6INGxZJQKuWbQB7dFEBJt22h8GcjOIlXvw4cKsgc3KvplIjTrFlnYiQQVvcSy+uQRXJTJTm2Y6vxOQzFvSJa0S8lXz5+x/CqpqXJtj1cSztEHZEFdBla2M30smV1uJvQcfa+lIRPwXdwtL0COsg8J00hAYBoFXPo+4N/jytArkYOFz6MasUrRODURuAE2fR6JI2aAerBy0WFE17TyWuXjWlnYt6t9aO5Wzo/qc/3DgMWHQr8NZGVg=="|});
  488. ("digest", {|SHA-256=bcLAcfJg/048pSOMeA29j/PqW2iQJw96mT+egmjF+Zk=|});
  489. ] |> Cohttp.Header.of_list in
  490. let pay = h |> Http.Signature.to_sign_string0
  491. ~request:(Some (`POST,
  492. "https://dev.seppo.social/2024-03-19/seppo.cgi/activitypub/inbox.jsa" |> Uri.of_string)) in
  493. pay |> Assrt.equals_string __LOC__ {|(request-target): post /2024-03-19/seppo.cgi/activitypub/inbox.jsa
  494. host: dev.seppo.social
  495. date: Mon, 16 Sep 2024 12:26:45 GMT
  496. signature: keyId="https://gotosocial.dev.seppo.social/users/demo/main-key",algorithm="hs2019",headers="(request-target) host date digest",signature="MG9tIV9rWJHDFKEFGsjakYoBtPjZbyk/ddTn6Xr2xHkmTVZDmkJmGcD4yDfWfQ4m8BYS+jd4lnb8O5fdm/pFwpFDGU70IDLsg6INGxZJQKuWbQB7dFEBJt22h8GcjOIlXvw4cKsgc3KvplIjTrFlnYiQQVvcSy+uQRXJTJTm2Y6vxOQzFvSJa0S8lXz5+x/CqpqXJtj1cSztEHZEFdBla2M30smV1uJvQcfa+lIRPwXdwtL0COsg8J00hAYBoFXPo+4N/jytArkYOFz6MasUrRODURuAE2fR6JI2aAerBy0WFE17TyWuXjWlnYt6t9aO5Wzo/qc/3DgMWHQr8NZGVg=="
  497. digest: SHA-256=bcLAcfJg/048pSOMeA29j/PqW2iQJw96mT+egmjF+Zk=|};
  498. let signature = "signature"
  499. |> Cohttp.Header.get h
  500. |> Option.value ~default:"ouch"
  501. |> Http.Signature.decode
  502. |> Result.get_ok
  503. |> List.assoc "signature" in
  504. signature |> check string __LOC__ {|MG9tIV9rWJHDFKEFGsjakYoBtPjZbyk/ddTn6Xr2xHkmTVZDmkJmGcD4yDfWfQ4m8BYS+jd4lnb8O5fdm/pFwpFDGU70IDLsg6INGxZJQKuWbQB7dFEBJt22h8GcjOIlXvw4cKsgc3KvplIjTrFlnYiQQVvcSy+uQRXJTJTm2Y6vxOQzFvSJa0S8lXz5+x/CqpqXJtj1cSztEHZEFdBla2M30smV1uJvQcfa+lIRPwXdwtL0COsg8J00hAYBoFXPo+4N/jytArkYOFz6MasUrRODURuAE2fR6JI2aAerBy0WFE17TyWuXjWlnYt6t9aO5Wzo/qc/3DgMWHQr8NZGVg==|};
  505. let signature = signature
  506. |> Base64.decode_exn
  507. |> Cstruct.of_string in
  508. let pe = {|{"@context":["https://www.w3.org/ns/activitystreams","http://schema.org","http://joinmastodon.org/ns","https://w3id.org/security/v1"],"attachment":[{"name":"reason","type":"PropertyValue","value":"\u003ca href=\"https://seppo.social/issues/13\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003ehttps://seppo.social/issues/13\u003c/a\u003e"},{"name":"2","type":"PropertyValue","value":"b"},{"name":"3","type":"PropertyValue","value":"c"},{"name":"4","type":"PropertyValue","value":"d"}],"discoverable":false,"featured":"https://gotosocial.dev.seppo.social/users/demo/collections/featured","followers":"https://gotosocial.dev.seppo.social/users/demo/followers","following":"https://gotosocial.dev.seppo.social/users/demo/following","id":"https://gotosocial.dev.seppo.social/users/demo","inbox":"https://gotosocial.dev.seppo.social/users/demo/inbox","manuallyApprovesFollowers":true,"name":"slöth","outbox":"https://gotosocial.dev.seppo.social/users/demo/outbox","preferredUsername":"demo","publicKey":{"id":"https://gotosocial.dev.seppo.social/users/demo/main-key","owner":"https://gotosocial.dev.seppo.social/users/demo","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxaVct4ctlG5ssph8+Qiu\ncV3/mtoTGE3nyl+9eTUaIvqdgcedIvDrAqJ4Koj6JEFMTjHpJYBvWXWhR7QEuYZJ\n3HUjcZozxh40zCOosu841uvoIGEDq75xHRKhFRC38RBo7PRhAjqqCYgfkt7AERmw\nJBA1BF2EAPI5i5mjo2IPZzAJdByweVZoo5Wo3Ki3utF3oPXZRnuHmtLbphdJxZTD\n9MXfaZLG6sfuzv+Og7O6AninawBmYFz60d8GwfaBOKi8ftbnheh04BGir+jgE02m\nWNgdMWOBM9D0D3+kmZ2+m+rACQCT9D500EtMWZSn4ZBDQxUkLLbXrZJt5/fDRur7\nQQIDAQAB\n-----END PUBLIC KEY-----\n"},"summary":"\u003cp\u003e\u003ca href=\"https://seppo.social/issues/13\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003ehttps://seppo.social/issues/13\u003c/a\u003e\u003c/p\u003e","tag":[],"type":"Person","url":"https://gotosocial.dev.seppo.social/@demo"}|}
  509. |> Ezjsonm.from_string
  510. |> As2_vocab.Decode.person
  511. |> Result.get_ok in
  512. let pub = pe.public_key.pem |> Ap.PubKeyPem.of_pem |> Result.get_ok in
  513. let `Msg e = X509.Public_key.verify
  514. `SHA256
  515. ~scheme:`RSA_PKCS1
  516. ~signature
  517. pub
  518. (`Message (pay |> Cstruct.of_string))
  519. |> Result.get_error in
  520. e |> check string __LOC__ "bad signature"
  521. let tc_verify_hs2019 () =
  522. (*
  523. 2024-09-16T14:26:45.455+02:00 DEBUG Is2s.Inbox.post ba111224-c9b7-4c56-ae48-82f04795f23e Signature: keyId="https://gotosocial.dev.seppo.social/users/demo/main-key",algorithm="hs2019",headers="(request-target) host date digest",signature="MG9tIV9rWJHDFKEFGsjakYoBtPjZbyk/ddTn6Xr2xHkmTVZDmkJmGcD4yDfWfQ4m8BYS+jd4lnb8O5fdm/pFwpFDGU70IDLsg6INGxZJQKuWbQB7dFEBJt22h8GcjOIlXvw4cKsgc3KvplIjTrFlnYiQQVvcSy+uQRXJTJTm2Y6vxOQzFvSJa0S8lXz5+x/CqpqXJtj1cSztEHZEFdBla2M30smV1uJvQcfa+lIRPwXdwtL0COsg8J00hAYBoFXPo+4N/jytArkYOFz6MasUrRODURuAE2fR6JI2aAerBy0WFE17TyWuXjWlnYt6t9aO5Wzo/qc/3DgMWHQr8NZGVg=="
  524. 2024-09-16T14:26:45.608+02:00 DEBUG Is2s.Inbox.post signature check '(request-target): post /2024-03-19/seppo.cgi/activitypub/inbox.jsa
  525. host: dev.seppo.social
  526. date: Mon, 16 Sep 2024 12:26:45 GMT
  527. digest: SHA-256=bcLAcfJg/048pSOMeA29j/PqW2iQJw96mT+egmjF+Zk='
  528. *)
  529. let act = {|{"@context":["https://www.w3.org/ns/activitystreams","http://schema.org","http://joinmastodon.org/ns","https://w3id.org/security/v1"],"attachment":[{"name":"reason","type":"PropertyValue","value":"\u003ca href=\"https://seppo.social/issues/13\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003ehttps://seppo.social/issues/13\u003c/a\u003e"},{"name":"2","type":"PropertyValue","value":"b"},{"name":"3","type":"PropertyValue","value":"c"},{"name":"4","type":"PropertyValue","value":"d"}],"discoverable":false,"featured":"https://gotosocial.dev.seppo.social/users/demo/collections/featured","followers":"https://gotosocial.dev.seppo.social/users/demo/followers","following":"https://gotosocial.dev.seppo.social/users/demo/following","id":"https://gotosocial.dev.seppo.social/users/demo","inbox":"https://gotosocial.dev.seppo.social/users/demo/inbox","manuallyApprovesFollowers":true,"name":"slöth","outbox":"https://gotosocial.dev.seppo.social/users/demo/outbox","preferredUsername":"demo","publicKey":{"id":"https://gotosocial.dev.seppo.social/users/demo/main-key","owner":"https://gotosocial.dev.seppo.social/users/demo","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxaVct4ctlG5ssph8+Qiu\ncV3/mtoTGE3nyl+9eTUaIvqdgcedIvDrAqJ4Koj6JEFMTjHpJYBvWXWhR7QEuYZJ\n3HUjcZozxh40zCOosu841uvoIGEDq75xHRKhFRC38RBo7PRhAjqqCYgfkt7AERmw\nJBA1BF2EAPI5i5mjo2IPZzAJdByweVZoo5Wo3Ki3utF3oPXZRnuHmtLbphdJxZTD\n9MXfaZLG6sfuzv+Og7O6AninawBmYFz60d8GwfaBOKi8ftbnheh04BGir+jgE02m\nWNgdMWOBM9D0D3+kmZ2+m+rACQCT9D500EtMWZSn4ZBDQxUkLLbXrZJt5/fDRur7\nQQIDAQAB\n-----END PUBLIC KEY-----\n"},"summary":"\u003cp\u003e\u003ca href=\"https://seppo.social/issues/13\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003ehttps://seppo.social/issues/13\u003c/a\u003e\u003c/p\u003e","tag":[],"type":"Person","url":"https://gotosocial.dev.seppo.social/@demo"}|} in
  530. let pe = act |> Ezjsonm.from_string |> As2_vocab.Decode.person |> Result.get_ok in
  531. let pub = pe.public_key.pem |> Ap.PubKeyPem.of_pem |> Result.get_ok in
  532. let h = [
  533. ("host", {|dev.seppo.social|});
  534. ("date", {|Mon, 16 Sep 2024 12:26:45 GMT|});
  535. ("signature", {|keyId="https://gotosocial.dev.seppo.social/users/demo/main-key",algorithm="hs2019",headers="(request-target) host date digest",signature="MG9tIV9rWJHDFKEFGsjakYoBtPjZbyk/ddTn6Xr2xHkmTVZDmkJmGcD4yDfWfQ4m8BYS+jd4lnb8O5fdm/pFwpFDGU70IDLsg6INGxZJQKuWbQB7dFEBJt22h8GcjOIlXvw4cKsgc3KvplIjTrFlnYiQQVvcSy+uQRXJTJTm2Y6vxOQzFvSJa0S8lXz5+x/CqpqXJtj1cSztEHZEFdBla2M30smV1uJvQcfa+lIRPwXdwtL0COsg8J00hAYBoFXPo+4N/jytArkYOFz6MasUrRODURuAE2fR6JI2aAerBy0WFE17TyWuXjWlnYt6t9aO5Wzo/qc/3DgMWHQr8NZGVg=="|});
  536. ("digest", {|SHA-256=bcLAcfJg/048pSOMeA29j/PqW2iQJw96mT+egmjF+Zk=|});
  537. ] |> Cohttp.Header.of_list in
  538. h |> Http.Signature.to_sign_string0 ~request:(Some (`POST,"/" |> Uri.of_string))
  539. |> Assrt.equals_string __LOC__ {|(request-target): post /
  540. host: dev.seppo.social
  541. date: Mon, 16 Sep 2024 12:26:45 GMT
  542. signature: keyId="https://gotosocial.dev.seppo.social/users/demo/main-key",algorithm="hs2019",headers="(request-target) host date digest",signature="MG9tIV9rWJHDFKEFGsjakYoBtPjZbyk/ddTn6Xr2xHkmTVZDmkJmGcD4yDfWfQ4m8BYS+jd4lnb8O5fdm/pFwpFDGU70IDLsg6INGxZJQKuWbQB7dFEBJt22h8GcjOIlXvw4cKsgc3KvplIjTrFlnYiQQVvcSy+uQRXJTJTm2Y6vxOQzFvSJa0S8lXz5+x/CqpqXJtj1cSztEHZEFdBla2M30smV1uJvQcfa+lIRPwXdwtL0COsg8J00hAYBoFXPo+4N/jytArkYOFz6MasUrRODURuAE2fR6JI2aAerBy0WFE17TyWuXjWlnYt6t9aO5Wzo/qc/3DgMWHQr8NZGVg=="
  543. digest: SHA-256=bcLAcfJg/048pSOMeA29j/PqW2iQJw96mT+egmjF+Zk=|};
  544. let si = "signature"
  545. |> Cohttp.Header.get h |> Option.value ~default:"ouch"
  546. |> Http.Signature.decode
  547. |> Result.get_ok in
  548. si |> List.length |> check int __LOC__ 4;
  549. si |> List.assoc "keyId" |> check string __LOC__ "https://gotosocial.dev.seppo.social/users/demo/main-key";
  550. si |> List.assoc "algorithm" |> check string __LOC__ "hs2019";
  551. let sign = si |> List.assoc "signature" |> Base64.decode_exn |> Cstruct.of_string in
  552. let inbox = "/" |> Uri.of_string in
  553. let `Msg m = h
  554. |> Http.Signature.to_sign_string0 ~request:(Some (`GET,Uri.empty))
  555. |> Cstruct.of_string
  556. |> Ap.PubKeyPem.verify ~algo:"hs2019" ~inbox ~key:pub ~signature:sign
  557. |> Result.get_error in
  558. m |> check string __LOC__ "bad signature"
  559. (* also https://datatracker.ietf.org/doc/id/draft-richanna-http-message-signatures-00.html#section-a.3
  560. 2024-10-03T00:14:44.022+02:00 DEBUG Is2s.Inbox.post 837a994a-754b-49bd-9154-982603e3dcc7 Signature: keyId="https://gotosocial.dev.seppo.social/users/demo/main-key",algorithm="hs2019",headers="(request-target) host date digest",signature="HCnGYTDX+xfmuUysOGBN//mqInBv//55S/1NRZ+vEqMZxIgu7QsmUB/I7MfeM3PyF1oZDZ5cngsLPmuSjBVnQkAOJIebybNsh9HyLT5Ln4UDyiY30AZVuX+tNz0K5eGmnxS9LFPyfihvrnYZN+2Irny5mCPkB61u8TTmjYKG/WTLKrVhf49fwNt6U11zq7xkVkB8NT6VllH4tftx/GpfqvdCl+FA+UrtKu6GyHBQMmsEz7ybcVXhF04K8z95X2nM/I/pfmQ/b2ySpzX3YwL0UlVrFI44fq7zXIvpkKT3ntg66z3xluuhBL3y2amzty6Ciz/evcJcq6JpaVJ2jTNO5Q=="
  561. 2024-10-03T00:14:44.205+02:00 DEBUG Is2s.Inbox.post signature check '(request-target): post /2024-03-19/seppo.cgi/activitypub/inbox.jsa
  562. host: mx250.darknet.mro.name
  563. date: Wed, 02 Oct 2024 22:14:43 GMT
  564. digest: SHA-256=DpWuW0JAXqss/tShNxYG7NmD+o18Ok7lNDvHRD0vbcU='
  565. *)
  566. let tc_verify_hs2019_b () =
  567. let signature = {|HCnGYTDX+xfmuUysOGBN//mqInBv//55S/1NRZ+vEqMZxIgu7QsmUB/I7MfeM3PyF1oZDZ5cngsLPmuSjBVnQkAOJIebybNsh9HyLT5Ln4UDyiY30AZVuX+tNz0K5eGmnxS9LFPyfihvrnYZN+2Irny5mCPkB61u8TTmjYKG/WTLKrVhf49fwNt6U11zq7xkVkB8NT6VllH4tftx/GpfqvdCl+FA+UrtKu6GyHBQMmsEz7ybcVXhF04K8z95X2nM/I/pfmQ/b2ySpzX3YwL0UlVrFI44fq7zXIvpkKT3ntg66z3xluuhBL3y2amzty6Ciz/evcJcq6JpaVJ2jTNO5Q==|}
  568. |> Base64.decode_exn |> Cstruct.of_string
  569. and key = {|-----BEGIN PUBLIC KEY-----
  570. MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxaVct4ctlG5ssph8+Qiu
  571. cV3/mtoTGE3nyl+9eTUaIvqdgcedIvDrAqJ4Koj6JEFMTjHpJYBvWXWhR7QEuYZJ
  572. 3HUjcZozxh40zCOosu841uvoIGEDq75xHRKhFRC38RBo7PRhAjqqCYgfkt7AERmw
  573. JBA1BF2EAPI5i5mjo2IPZzAJdByweVZoo5Wo3Ki3utF3oPXZRnuHmtLbphdJxZTD
  574. 9MXfaZLG6sfuzv+Og7O6AninawBmYFz60d8GwfaBOKi8ftbnheh04BGir+jgE02m
  575. WNgdMWOBM9D0D3+kmZ2+m+rACQCT9D500EtMWZSn4ZBDQxUkLLbXrZJt5/fDRur7
  576. QQIDAQAB
  577. -----END PUBLIC KEY-----
  578. |} |> Cstruct.of_string
  579. |> X509.Public_key.decode_pem |> Result.get_ok
  580. and data = [ {|(request-target): post /2024-03-19/seppo.cgi/activitypub/inbox.jsa|} ;
  581. {|host: mx250.darknet.mro.name|} ;
  582. {|date: Wed, 02 Oct 2024 22:14:43 GMT|} ;
  583. {|digest: SHA-256=DpWuW0JAXqss/tShNxYG7NmD+o18Ok7lNDvHRD0vbcU=|} ]
  584. in
  585. key |> X509.Public_key.key_type |> X509.Key_type.to_string |> check string __LOC__ "rsa";
  586. X509.Public_key.verify
  587. `SHA256
  588. ~scheme:`RSA_PKCS1
  589. ~signature
  590. key
  591. (`Message (data |> String.concat "\n" |> Cstruct.of_string))
  592. |> Result.get_ok
  593. (* also
  594. https://datatracker.ietf.org/doc/id/draft-richanna-http-message-signatures-00.html#example-key-rsa-test
  595. *)
  596. let pub_key_richanna =
  597. {|-----BEGIN PUBLIC KEY-----
  598. MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhAKYdtoeoy8zcAcR874L
  599. 8cnZxKzAGwd7v36APp7Pv6Q2jdsPBRrwWEBnez6d0UDKDwGbc6nxfEXAy5mbhgaj
  600. zrw3MOEt8uA5txSKobBpKDeBLOsdJKFqMGmXCQvEG7YemcxDTRPxAleIAgYYRjTS
  601. d/QBwVW9OwNFhekro3RtlinV0a75jfZgkne/YiktSvLG34lw2zqXBDTC5NHROUqG
  602. TlML4PlNZS5Ri2U4aCNx2rUPRcKIlE0PuKxI4T+HIaFpv8+rdV6eUgOrB2xeI1dS
  603. FFn/nnv5OoZJEIB+VmuKn3DCUcCZSFlQPSXSfBDiUGhwOw76WuSSsf1D4b/vLoJ1
  604. 0wIDAQAB
  605. -----END PUBLIC KEY-----
  606. |}
  607. |> Cstruct.of_string |> X509.Public_key.decode_pem |> Result.get_ok
  608. (* example from
  609. https://datatracker.ietf.org/doc/id/draft-richanna-http-message-signatures-00.html#name-hs2019-signature-over-minim
  610. *)
  611. let tc_verify_hs2019_richanna_minimal () =
  612. let signature = "e3y37nxAoeuXw2KbaIxE2d9jpE7Z9okgizg6QbD2Z7fUVUvog+ZTKKLRBnhNglVIY6fAaYlHwx7ZAXXdBVF8gjWBPL6U9zRrB4PFzjoLSxHaqsvS0ZK9FRxpenptgukaVQ1aeva3PE1aD6zZ93df2lFIFXGDefYCQ+M/SrDGQOFvaVykEkte5mO6zQZ/HpokjMKvilfSMJS+vbvC1GJItQpjs636Db+7zB2W1BurkGxtQdCLDXuIDg4S8pPSDihkch/dUzL2BpML3PXGKVXwHOUkVG6Q2ge07IYdzya6N1fIVA9eKI1Y47HT35QliVAxZgE0EZLo8mxq19ReIVvuFg=="
  613. |> Base64.decode_exn |> Cstruct.of_string
  614. and key = pub_key_richanna
  615. and data = [ {|(created): 1402170695|} ;
  616. {|(request-target): post /foo?param=value&pet=dog|} ]
  617. in
  618. key |> X509.Public_key.key_type |> X509.Key_type.to_string |> check string __LOC__ "rsa";
  619. X509.Public_key.verify
  620. `SHA256
  621. ~scheme:`RSA_PKCS1
  622. ~signature
  623. key
  624. (`Message (data |> String.concat "\n" |> Cstruct.of_string))
  625. |> Result.get_ok
  626. (* example from
  627. https://datatracker.ietf.org/doc/id/draft-richanna-http-message-signatures-00.html#name-create-the-signature-input
  628. *)
  629. let tc_verify_hs2019_richanna_nonn () =
  630. let signature = {|T1l3tWH2cSP31nfuvc3nVaHQ6IAu9YLEXg2pCeEOJETXnlWbgKtBTaXV6LNQWtf4O42V2DZwDZbmVZ8xW3TFW80RrfrY0+fyjD4OLN7/zV6L6d2v7uBpuWZ8QzKuHYFaRNVXgFBXN3VJnsIOUjv20pqZMKO3phLCKX2/zQzJLCBQvF/5UKtnJiMp1ACNhG8LF0Q0FPWfe86YZBBxqrQr5WfjMu0LOO52ZAxi9KTWSlceJ2U361gDb7S5Deub8MaDrjUEpluphQeo8xyvHBoNXsqeax/WaHyRYOgaW6krxEGVaBQAfA2czYZhEA05Tb38ahq/gwDQ1bagd9rGnCHtAg==|}
  631. |> Base64.decode_exn |> Cstruct.of_string
  632. and key = pub_key_richanna
  633. and data = {|(request-target): get /foo
  634. (created): 1402170695
  635. host: example.org
  636. date: Tue, 07 Jun 2014 20:51:35 GMT
  637. cache-control: max-age=60, must-revalidate
  638. x-emptyheader:
  639. x-example: Example header with some whitespace.|}
  640. in
  641. key |> X509.Public_key.key_type |> X509.Key_type.to_string |> check string __LOC__ "rsa";
  642. X509.Public_key.verify
  643. `SHA256
  644. ~scheme:`RSA_PKCS1
  645. ~signature
  646. key
  647. (`Message (data |> Cstruct.of_string))
  648. |> Result.get_ok
  649. (* example from
  650. https://gts.superseriousbusiness.org/@dumpsterqueer/statuses/01J9BKYKR1W5X078ZESSAW10QS
  651. *)
  652. let tc_verify_hs2019_gotosocial () =
  653. let signature = {|G6X7IV+qHqOaZIrrwRxunzbRgRhzn84UoUJfsSNLveHdVBAiaY3ayoj2F4ZiDxVV6zG0CN3j+0pHbngWgHp4aMETkF/x3KB/l2ILHgBhUpIB+ZAb1MkC+yU+9BNmp8EmVZldzdjQ/MalStfeRc7rcMdL770TJbAW8cgPRPA6TB7P6m5tzEPkow56wR/W0MuYJqWQzE8id7Ri65p63fu8NFha7WgBVM5I+67hZ3sYZTBKdLQJJyS4K3nOZ20h+pUSZUGF7WdTNnxtzaryJgFVL4Or2ydBa/Jp0w8zspWFMlGCtG9A9cayQ7JHlCMiuf92f/hpLWtWCSftg9IzZVakiw==|}
  654. |> Base64.decode_exn |> Cstruct.of_string
  655. and key =
  656. "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy2/j9+2G7xrQvBtygrj4\naYHl8hTeZecDBnS+6IBjjEt+QWJ3z0Cv9lXSVMZw5i6DVTkVOGZrh2vZDu0BCTEV\n07dyASArE63Qe21WwjNObpkQ7YZbMxUkYjWCDYdqLMifAqElYzIK7xnY0pTWylmC\njm39qxmhk22PpzRkw+zofh9ykqyadmkA2/KrpZgGnjn6MiPqb2DeELV1tzmZ7mAz\n+k7pkkhxvBVqPhCZ104pyd1lc66obONSnIqxugRlrrUZbFv1e6xFsUmMUYrAGQTt\nZr4VeZwuaYj/MvFIeOZrmth/lg3QpYbKZYnJKVePyH+530jRSFerr2unbuGmEQAg\nvQIDAQAB\n-----END PUBLIC KEY-----\n"
  657. |> Cstruct.of_string |> X509.Public_key.decode_pem |> Result.get_ok
  658. and data = {|(request-target): get /@mro/113244470620401979
  659. host: digitalcourage.social
  660. date: Fri, 04 Oct 2024 11:12:53 GMT|}
  661. in
  662. key |> X509.Public_key.key_type |> X509.Key_type.to_string |> check string __LOC__ "rsa";
  663. let `Msg m = X509.Public_key.verify
  664. `SHA256
  665. ~scheme:`RSA_PKCS1
  666. ~signature
  667. key
  668. (`Message (data |> Cstruct.of_string))
  669. |> Result.get_error in
  670. m
  671. |> check string __LOC__ "bad signature"
  672. let tc_verify_masto430 () =
  673. (*
  674. Signature: keyId="https://bewegung.social/use
  675. rs/mro#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="v2h3HUJg0vH6HyNfOHSE7hIg0O
  676. i1E+iUEO8ahxDjI99F7jKuh9sVRSpqhsAEagI5WeWZkQyhWjRmDZZBsk4+acALo36CsRj/C/m5CiF4J0hd+x8VSPWDJEQTYclm0jCthfdmeXg1/DOZnlWInBVQdwZJZyoW7nTn
  677. EEGZuE0w6LsYCJb2oVUTW32gn+fbHnJ2mkBwPjwBlJ7zckEx3MwnV99GPkjdA0hBX/O4xSD7a0MIn4d0CSOGmbnTGKChTm//AvKXP4L5H9L6ovZFBfaHkDCqYDbdfXGWeheLXd
  678. gDJHubi0LFecP3PP5cwOeuFtGgkWsSeLrUEyWgSlEKjCFRhw=="
  679. (request-target): post /2024-03-19/seppo.cgi/activitypub/inbox.jsa
  680. (request-target): post /2024-03-19/seppo.cgi/activitypub/inbox.jsa
  681. host: mx250.darknet.mro.name
  682. date: Thu, 10 Oct 2024 10:12:10 GMT
  683. digest: SHA-256=N0m+gyYe/GieNBzMEOStVqf7hq/qdmh7bqdWanZSE1o=
  684. content-type: application/activity+json
  685. *)
  686. ()
  687. let tc_sign_api () =
  688. (*
  689. sign != verify
  690. but has overlap
  691. constructing the signagee payload
  692. - keys
  693. - values
  694. -
  695. signing ig
  696. verifying it
  697. *)
  698. (** @TODO *)
  699. let _list_header_get l (k : string) : string option = l |> List.assoc_opt k in
  700. let compute_verify_signature_payload fetcher : string =
  701. (match fetcher "signature" with
  702. | None -> Some "header not found: signature"
  703. | Some si ->
  704. match si |> Http.Signature.decode with
  705. | Error _ -> Some "error decoding signature"
  706. | Ok si ->
  707. match si |> List.assoc_opt "headers" with
  708. | None -> Some "signature field not found: headers"
  709. | Some v ->
  710. (* assert lowercase *)
  711. v
  712. |> Astring.String.cuts ~sep:" " |> List.rev |> List.fold_left
  713. (fun init k -> match k |> fetcher with
  714. | None -> init
  715. | Some v -> (k,v) :: init) []
  716. |> Cohttp.Header.of_list
  717. |> Cohttp.Header.to_frames
  718. |> Astring.String.concat ~sep:"\n"
  719. |> Option.some )
  720. |> Option.value ~default:""
  721. in
  722. let r = {Cgi.Request.empty with
  723. request_method = "POST";
  724. raw_string = (function
  725. | "HTTP_HOST" -> Some "example.com"
  726. | "HTTP_DATE" -> Some "tomorrow"
  727. | "HTTP_SIGNATURE" -> Some {|keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date",signature="qdx+H7PHHDZgy4y/Ahn9Tny9V3GP6YgBPyUXMmoxWtLbHpUnXS2mg2+SbrQDMCJypxBLSPQR2aAjn7ndmw2iicw3HMbe8VfEdKFYRqzic+efkb3nndiv/x1xSHDJWeSWkx3ButlYSuBskLu6kd9Fswtemr3lgdDEmn04swr2Os0="|}
  728. | _ -> None)} in
  729. Cgi.Request.header_get r "SIGNATURE" |> Option.get
  730. |> Http.Signature.decode |> Result.get_ok
  731. |> List.assoc "headers"
  732. |> Astring.String.cuts ~sep:" "
  733. |> List.length
  734. |> Assrt.equals_int __LOC__ 3;
  735. (* typical for verify *)
  736. r
  737. |> Cgi.Request.header_get
  738. |> compute_verify_signature_payload
  739. |> Assrt.equals_string __LOC__ {|(request-target): post /
  740. host: example.com
  741. date: tomorrow|};
  742. ();
  743. ()
  744. end
  745. let () =
  746. run
  747. "seppo_suite" [
  748. __FILE__ , [
  749. "setup", `Quick, set_up;
  750. "tc_relpa", `Quick, tc_relpa;
  751. "tc_uri", `Quick, tc_uri;
  752. "tc_rel_cd", `Quick, tc_rel_cd;
  753. "tc_rx_script_name", `Quick, Request.tc_rx_script_name;
  754. "tc_uri", `Quick, Request.tc_uri;
  755. "tc_base", `Quick, Request.tc_base;
  756. "tc_query_string", `Quick, Request.tc_query_string;
  757. "tc_rfc1123", `Quick, Cookie.tc_rfc1123;
  758. "tc_to_string", `Quick, Cookie.tc_to_string;
  759. "tc_of_string", `Quick, Cookie.tc_of_string;
  760. "tc_headers", `Quick, Header.tc_headers;
  761. "tc_sig_encode", `Quick, Header.tc_sig_encode;
  762. "tc_signature", `Quick, Header.tc_signature;
  763. "tc_sign2", `Quick, Header.tc_sign2;
  764. "tc_to_sign_string_basic", `Quick, Header.tc_to_sign_string_basic;
  765. "tc_sign_basic", `Quick, Header.tc_sign_basic;
  766. "tc_sign_all_headers", `Quick, Header.tc_sign_all_headers;
  767. "tc_signed_headers", `Quick, Header.tc_signed_headers;
  768. "tc_parse_auth_params", `Quick, Header.tc_parse_auth_params;
  769. "tc_parse_signature", `Quick, Header.tc_parse_signature;
  770. "tc_verify_basic", `Quick, Header.tc_verify_basic;
  771. "tc_verify_hs2019_raw", `Quick, Header.tc_verify_hs2019_raw;
  772. "tc_verify_hs2019", `Quick, Header.tc_verify_hs2019;
  773. "tc_verify_hs2019_b", `Quick, Header.tc_verify_hs2019_b;
  774. "tc_verify_hs2019_richanna_minimal", `Quick, Header.tc_verify_hs2019_richanna_minimal;
  775. "tc_verify_hs2019_richanna_nonn", `Quick, Header.tc_verify_hs2019_richanna_nonn;
  776. "tc_verify_hs2019_gotosocial", `Quick, Header.tc_verify_hs2019_gotosocial;
  777. "tc_verify_masto430", `Quick, Header.tc_verify_masto430;
  778. "tc_sign_api", `Quick, Header.tc_sign_api;
  779. ]
  780. ]