Schlez
2018
RS256 in ReasonML/OCaml

RS256 in ReasonML/OCaml

It took me a decent amount of time figuring out how to sign messages for a ReasonML GitHub app. Here are my findings.Published .

Just before we publish new versions of Yoshi, Wix’s internal development toolchain, we generate our CHANGELOG.md file using the wonderful lerna-changelog, and follow their assumptions: “lerna-changelog will show all pull requests that have been merged since the latest tagged commit in the repository. That is however only true for pull requests with certain labels applied”. GitHub labels are shared between PRs and issues, but only some of them are used for the changelog generation.

In order to reduce confusion, we chose to have PR: prefix to all labels that affects the changelog, so maintainers would know to choose at least one of them for each PR.

Sometimes, we forget to add a label, and that causes the PR to not be in the changelog. That may be totally fine for some projects, but we want to be as transparent as we can, therefore “hiding” a PR hurts our core values as a team.

A native ReasonML GitHub app

I decided to write a simple app that will add a status check that passes only if the PR has a valid changelog label, triggered by GitHub’s webhooks. As a Reason enthusiast, I wanted to try developing it in native Reason, so I’ll learn how to use the native OCaml toolchain better. So, instead of compiling to JavaScript and running it with Node.js, like most tutorials around Reason show, I decided to use native OCaml and produce a static binary.

This creates a lightweight executable (11MB!) that can be deployed very fast compared to standard Node apps, that need the entire Node runtime installed. The source code is available on GitHub, for those who want to join in or just take a look.

At first, all I did was making a simple server that works with GitHub webhooks that accept a user token as a query parameter that I’ll later pass to the access_token query param in the GitHub API. While this works well, it is “less secure”, because all the repository admins will have access to some user’s token. Generating a user just to make API calls is also less secure than just using GitHub apps. So I decided to go with GitHub apps.

The only difference between using webhooks to GitHub apps is an authorization process. Apps are using RS256 signed JWT, as mentioned in the docs to request a short-living token for a specific installation (or, a repo). Then, the rest is the same — we get a token, just like we got from the query param.

In order to make JWTs I tried to use the ocaml-jwt library, but unfortunately, it doesn’t support RS256 out of the box, although there is a PR that adds the support, so I decided to do that from “scratch”: searching for a library to sign my request and build the JWT myself.

Since I’m pretty new to OCaml, I’m not aware of libraries so I’m just googling around and looking for packages. After a long time of research, I decided to go with nocrypto (along with x509), when I saw a wild github repo called ocaml-letsencrpt implementing exactly what I needed — A function that takes a private key, a string and returns a signed string. Here’s the function in a Reason syntax:

let rs256_sign = (key, data) => {
  /* Taken from https://github.com/mmaker/ocaml-letsencrypt */
  let data = Cstruct.of_string(data);
  let h = Nocrypto.Hash.SHA256.digest(data);
  let pkcs1_digest = X509.Encoding.pkcs1_digest_info_to_cstruct((`SHA256, h));
  Nocrypto.Rsa.PKCS1.sig_encode(~key, pkcs1_digest) |> Cstruct.to_string;
};

I’m using 3 libraries here: nocrypto, x509, and cstruct. So make sure you install them before using it in your project:

$ opam install nocrypto x509 cstruct

The function above takes a private key and a string, then signs the string. So we need to get the private key, and specifically a Nocrypto private key. Here’s a fast way of reading a PEM file into a Nocrypto RSA private key:

let read_file = path => {
  let ic = open_in(path);
  let len = in_channel_length(ic);
  let s = really_input_string(ic, len);
  close_in(ic);
  s;
};

let `RSA(key) =
  read_file("./github.private-key.pem")
  |> Cstruct.of_string
  |> X509.Encoding.Pem.Private_key.of_pem_cstruct1;

Now we can simply take our private key (which is stored in the key variable) and sign requests. The rest of the JWT-generation code lives here. It’s pretty straight-forward, yet hacky — I am using hard-coded stringified JSON, and I’m not even sorry — it feels secure because of the type checking. It formats some ints, so no escape necessary for JSON. Feels right.


As I already mentioned before, documentation and entry barrier aren’t the strongest sides of OCaml/Reason land, so I hope this will help others, or maybe even the future me.

Read more