TLS signature with opam:tls

how would I sign string, namely do the equivalent of

Base64.strict_encode64(keypair.sign(OpenSSL::Digest::SHA256.new, signed_string))

from How to implement a basic ActivityPub server - Official Mastodon Blog with the pure OCaml tls?

I can’t find sign or the like in index (tls.index)

1 Like

meanwhile found PSS (mirage-crypto-pk.Mirage_crypto_pk.Rsa.PSS) but frankly it sounds a bit greek to me.

Will try along mirage-crypto/test_rsa.ml at main · mirage/mirage-crypto · GitHub

It is not entirely clear to me which kind of signatures AcitvityPub uses – I found reference to draft-ietf-httpbis-message-signatures-08 (in SocialCG/ActivityPub/Authentication Authorization - W3C Wiki):

RSA-PSS with SHA-512:

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
3 Likes

the latter looks like a hit - will try asap and report back here. Allow 48h.

Thanks so much for implementing TLS in OCaml :camel:!

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.

1 Like
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
2 Likes

indeed I do and I am raising funds (e.g. apply at prototypefund.de) to be able to do it professionally.
I am aiming for a single-user, zero-admin, self-hosted OCaml successor of mro/ShaarliGo: 🌺 self-reliantly posting on the #Fediverse with painless hosting and security in mind: 1. Rent any web space from EUR 2 mo… - Codeberg.org – but now integrated in the Fediverse. I looked round and found nothing so far.

I shall open the now closed repo around June at Marcus Rohrmoser 🌻 - Codeberg.org and mirrors at sourcehut, github etc.

2 Likes

thank you @hannes - that got me over the hill to a final

let test_sign_sha256 () =
  let open Lwt in
  let p =
    (*
$ openssl genrsa -out private.pem 2048
$ openssl rsa -in private.pem -outform PEM -pubout -out public.pem
    *)
    (* https://mirleft.github.io/ocaml-tls/doc/tls/X509_lwt/#val-private_of_pems *)
    X509_lwt.private_of_pems ~cert:"public.pem" ~priv_key:"private.pem"
    >>= fun (_, priv) ->
    let data : string = "to-be-signed-data" in
    let signature : string =
      (*
    https://mirleft.github.io/ocaml-x509/doc/x509/X509/Private_key/#cryptographic-sign-operation
    *)
      X509.Private_key.sign `SHA256 ~scheme:`RSA_PKCS1 priv
        (`Message (Cstruct.of_string data))
      |> Result.get_ok |> Cstruct.to_string |> Base64.encode_exn
    in
    signature
    |> Assert2.equals_string "sig 256"
         "TVMQvS8OZ94BFvMn8ToL0jG01L1T3Dww4o7R6NwcJd7KsOmZtUKzzCezbnY5gjSECj/cfXxs2mrZlk9xGntTKqhJ6YIZmM3BBdXuPl8IyWms/ qtqZ4d+NVfMVDhYeGm43+j2HTegpcH2px9auXSThd2WcJmc7J98g9hx5+pEr6hA2UjawzOPYxIyyhNHzX9L1hTu6Xyjq6OkPWgqK9aHnAnGG1f3LgH+     YTR0T/l5ODPCyKboFMfvmnQ2PDNRPgsz82j9YuMVF2sE/TCdpTg+T6dX99Hmp35lomXnf1GSTrVAWBcx6mFEOABMrFSRRcMzGo9zCWPb/               y8V3xWaSpjroQ==";
    return ()
  in
  Lwt_main.run p

Feels good.

3 Likes

just implemented key generation seppo/as2.ml at develop - seppo - Codeberg.org

signing: https://codeberg.org/mro/seppo/src/commit/bfce4968205b1c1832fc2a51f8d6e33a58b15516/lib/as2.ml#L170

currently I’m struggling with ActivityPub HTTP signing but think the issue rather is in the actor json brittleness than the signing.
https://codeberg.org/mro/seppo/src/commit/bfce4968205b1c1832fc2a51f8d6e33a58b15516/lib/as2.ml#L358

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:

module Webfinger = struct

  let ty = function
    | `ActivityJson -> E.string Constants.ContentType.activity_json
    | `Html -> E.string Constants.ContentType.html
    | `ActivityJsonLd -> E.string Constants.ContentType.ld_json_activity_streams
    | `Json -> E.string Constants.ContentType.plain_json

  let link = function
    | Types.Webfinger.Self (t, href) -> obj [
      "href" @ href <: E.string;
      "rel" @ Constants.Webfinger.self_rel <: E.string;
      "type" @ t <: ty;
    ]
    | ProfilePage (t, href) ->
      obj [
        "href" @ href <: E.string;
        "rel" @ Constants.Webfinger.profile_page <: E.string;
        "type" @ t <: ty;
      ]
    | OStatusSubscribe template -> obj [
      "rel" @ Constants.Webfinger.profile_page <: E.string;
      "template" @ template <: E.string;
    ]

  let query_result ({subject;aliases;links}: Types.Webfinger.query_result) =
    obj [
      "subject" @ subject <: E.string;
      "aliases" @ aliases <: E.(list string);
      "links" @ links <: E.list link;
    ]

end

or announce activities:

let announce enc ({ id; actor; published; to_; cc; obj; raw=_ } : _ Types.announce) =
  ap_obj "Announce" [
    "id" @ id <: E.string;
    "actor" @ actor <: E.string;
    "published" @? published <: ptime;
    "to" @ to_ <: E.(list string);
    "cc" @ cc <: E.(list string);
    "object" @ obj <: enc;
  ]

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?

1 Like

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?

Can I see your code in full somewhere?

single file cgi https://codeberg.org/mro/seppo/src/branch/develop

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.

1 Like

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.

1 Like

how do you make sure your implementation can actually federate with
other servers?

Try and retry over and over again.

Get a taste here where friendly souls help me out:
https://socialhub.activitypub.rocks/t/we-have-created-documentation-on-how-exactly-lemmy-federation-works/1085/52?u=mro

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.

1 Like