It took me a decent amount of time figuring out how to sign messages for a ReasonML GitHub app. Here are my findings.
tl;dr: code snippets are below, the reason-native labels checker service is open source on GitHub
Just before we publish new versions of Yoshi, Wix’s internal development toolchain, we generate our
CHANGELOG.md file using the wonderful lerna-changelog. “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.
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. I wanted to write it in native Reason, so I’ll learn how to use the native OCaml toolchain better. So, instead of compiling to JS 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 runtime. The source code is 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:
I’m using 3 libraries here:
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:
Now we can simply take our private key 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. Don’t hesitate to ping me on twitter or just respond here. I’d love to chat!