module PSS_SHA512 = Mirage_crypto_pk.Rsa.PSS(Mirage_crypto.Hash.SHA512)
let priv = Mirage_crypto_pk.Rsa.generate ~bits:2048 () in
let data = "to-be-signed-data" in
let signature = PSS_SHA512.sign ~key:priv (`Data (Cstruct.of_string data)) in
Cstruct.to_string signature
(if you need PSS_SHA256, use module PSS_SHA256 = Mirage_crypto_pk.Rsa.PSS(Mirage_crypto.Hash.SHA256) instead).
For PKCS1 (using SHA256):
let priv = Mirage_crypto_pk.Rsa.generate ~bits:2048 () in
let data = "to-be-signed-data" in
let signature = Mirage_crypto_pk.Rsa.PKCS1.sign ~hash:`SHA256 ~key:priv (`Data (Cstruct.of_string data)) in
Cstruct.to_string signature
please bear with my baby steps, loading the keys as pem(s) seems to be incompabible with the sign call. As a whole:
let test_sign_sha256 () =
let open Lwt in
X509_lwt.private_of_pems ~cert:"test/public.pem" ~priv_key:"test/private.pem"
>>= fun (_, priv) ->
let data = "to-be-signed-data" in
let signature =
Mirage_crypto_pk.Rsa.PKCS1.sign ~hash:`SHA256 ~key:priv
(`Message (Cstruct.of_string data))
in
signature |> Cstruct.to_string |> Assert2.equals_string "sig" "sig"
yields
dune runtest
File "test/tls_test.ml", line 20, characters 55-59:
20 | Mirage_crypto_pk.Rsa.PKCS1.sign ~hash:`SHA256 ~key:priv
^^^^
Error: This expression has type X509.Private_key.t
but an expression was expected of type Mirage_crypto_pk.Rsa.priv
make: *** [test] Error 1
and I am puzzling how to either turn the loaded key priv into a Mirage_crypto_pk.Rsa.priv or load the latter from pems directly.
@mro Are you implementing an activitypub server? I was actually playing around with doing the same, and had run into the exact same question - In fact, I think I was also exactly following the join-mastadon blog post as a starting point.
(If so, would love to contribute btw).
I managed to get the types to match up at least by using X509.Private.sign or something to the same, and from looking at the implementation of some other activitypub servers (microblog.pub in particular), I believe this is equivalent to how they implement the signing as well? I think.
let test_sign_sha256 () =
let open Lwt in
X509_lwt.private_of_pems ~cert:"test/public.pem" ~priv_key:"test/private.pem"
>>= fun (_, priv) ->
indeed priv is a X509.Private_key.t. I can see two options:
(a)
let test_sign_sha256 () =
let open Lwt in
X509_lwt.private_of_pems ~cert:"test/public.pem" ~priv_key:"test/private.pem"
>>= fun (_, priv) ->
let data = "to-be-signed-data" in
let signature =
X509.Private_key.sign `SHA256 ~scheme:`RSA_PKCS1 priv (`Message (Cstruct.of_string data))
in
signature |> Cstruct.to_string |> Assert2.equals_string "sig" "sig"
(b)
let test_sign_sha256 () =
let open Lwt in
X509_lwt.private_of_pems ~cert:"test/public.pem" ~priv_key:"test/private.pem"
>>= function
| (_, `RSA priv) ->
let data = "to-be-signed-data" in
let signature =
Mirage_crypto_pk.Rsa.PKCS1.sign ~hash:`SHA256 ~key:priv
(`Message (Cstruct.of_string data))
in
signature |> Cstruct.to_string |> Assert2.equals_string "sig" "sig"
| _ -> assert false
Heyo @mro! Not sure if this will fix the issue you’re running into, but I used the following code to implement signing:
module Http_sig = struct
(* constructs a signed string *)
let build_signed_string ~signed_headers ~meth ~path ~headers ~body_digest =
String.split_on_char ' ' signed_headers
|> List.map (function
| "(request-target)" ->
"(request-target): " ^ String.lowercase_ascii meth ^ " " ^ path
| "digest" -> "digest: " ^ body_digest
| header -> header ^ ": " ^ (StringMap.find_opt header headers |> Option.value ~default:"")
)
|> String.concat "\n"
let build_signed_headers ~priv_key ~key_id
~headers ~body_str ~current_time
~method_ ~uri =
let signed_headers = "(request-target) content-length host date digest" in
let body_str_len = String.length body_str |> Int.to_string in
let body_digest = body_digest body_str in
let date = Http_date.to_utc_string current_time in
let host = uri
|> Uri.host
|> Option.get_exn_or "no host for request" in
let signature_string =
let to_be_signed =
build_signed_string
~signed_headers
~meth:(method_ |> String.lowercase_ascii)
~path:(Uri.path uri)
~headers:(StringMap.add "content-length" body_str_len @@
StringMap.add "date" date @@
StringMap.add "host" host @@
headers)
~body_digest in
let signed_string = encrypt priv_key to_be_signed |> Result.get_exn in
Printf.sprintf {|keyId="%s",algorithm="rsa-sha256",headers="%s",signature="%s"|}
key_id signed_headers signed_string in
List.fold_left (fun map (k,v) -> StringMap.add k v map) headers
["Digest", body_digest;
"Date", date;
"Host", host;
"Signature", signature_string;
"Content-Length", body_str_len ]
|> StringMap.to_list
end
let signed_req f (key_id, priv_key) uri body_str =
let current_time = Ptime_clock.now () in
let headers =
Http_sig.build_signed_headers
~current_time ~method_:"POST" ~body_str
~headers:(Http_sig.StringMap.of_list [
"Content-Type", APConstants.ContentType.ld_json_activity_streams
]) ~key_id ~priv_key ~uri
|> Cohttp.Header.of_list in
f ~headers uri body_str
let req_post ~headers url body =
let body = Cohttp_lwt.Body.of_string body in
try
let+ pair =
Cohttp_lwt_unix.Client.post
~headers
~body
url in
Lwt_result.return pair
with exn -> Lwt.return (Result.of_exn exn)
let signed_post key uri body =
signed_req req_post key uri body
An example of using it to post a follow request:
let follow_remote_user config
(local: Database.LocalUser.t)
~username ~domain db: (unit,string) Lwt_result.t =
let+! remote = resolve_remote_user ~username ~domain db in
let+! follow_request = build_follow_request config local remote db in
let uri = Database.RemoteUser.inbox remote in
let key_id =
Database.LocalUser.username local
|> Configuration.Url.user_key config
|> Uri.to_string in
let priv_key =
Database.LocalUser.privkey local in
let+! resp, _ = signed_post (key_id, priv_key) uri follow_request in
match resp.status with
| `OK -> Lwt_result.return ()
| _ -> Lwt_result.fail "request failed"
(Apologies for the code quality - I have been procrastinating a bit on doing some spring cleaning for this codebase)
A comment about your json encoding - you might want to try the decoders library for encoding/decoding, the compositionality of the library makes it very straightforward to map specific concepts from activitypub to specific validating decoders/encoders that also give nice explanations of validation errors.
For example, this was the code I used to encode webfinger query responses:
Also, I’m a bit curious, if you don’t mind answering, how you are going about building your server?
In particular, while the activitypub specification is fairly comprehensive, there are several undocumented conventions that servers must follow in order to actually properly federate with most common implementations on the fediverse.
In my case, I’m doing this in a trial-and-error fashion - I host a pleroma server and my development server on two separate subdomains of a domain I own, and then gradually try out different activities between the servers, observing what fails, debugging and then updating my code to fix the error.
Just curious if you’re doing something similar, or if you’ve found a better way?
I intend to do most with static files – e.g. webfinger. The webserver has to redirect to a static resource. Also I will not use a database other than the filesystem. And there will be just one user per instance.
When it comes to validity, I do not want to validate. There are no official schemas, are there?
it is bloated, useless crap. Nothing is reliable. Neither inside the spec (every implementer may choose) nor in the community. See all the 404s, expired https certificates and run-out domains BEING PART OF THE SPEC. After a few years. It’s like made by smarty pants from elementary school. The whole signing e.g. is “experimental, do not use in production” as of the ietf standard itself, but not part of activitypub either.
If Twitter wanted a ‘standard’ to cripple the topic for all others forever it may not look different. Something weird must have happened between Activitystreams 1.0 and 2.0.
Yep, completely agree, I was trying to be a bit diplomatic there, but I’m glad at least to hear that I’m not the only one who has nightmares from trying to make heads or tails of the AP spec.
I see. I guess the question I have is how do you make sure your implementation can actually federate with other servers? For me, I have found this to be quite a laborious process of trial and error + reading the implementations of other popular AP servers to work out any quirks they have hard coded etc.
Oh sure, I’ve just been hacking on a server for a bit on a private repo for a while, but haven’t published it yet. If you can wait, I’ll just add the finishing touches and try and publish it publicly this weekend.
The same stupid mess you had to go through, I guess. Dozends of servers
I definitively neither want to look into nor operate with all their
dependency crap.
haven’t published it yet.
take your time, don’t hurry but I’m curious, indeed.
Yup, that thread whole thread is a good summary of the hells of building a properly federating AP server.
While I’m at it, just to answer your question at the end of that thread, AP servers usually look up the id of any object posted to them to verify that the object is real and hasn’t been spoofed by a malicious actor. For this reason, whenever you send a follow/like request, you have to store the exact object you sent locally, so that when the receiving server validates your request, you will be able to return the exact same object.