{"id":16729108,"url":"https://github.com/gbour/letsencrypt-erlang","last_synced_at":"2025-10-27T09:52:24.537Z","repository":{"id":2853405,"uuid":"47639008","full_name":"gbour/letsencrypt-erlang","owner":"gbour","description":"Let's Encrypt client library for Erlang","archived":false,"fork":false,"pushed_at":"2020-06-26T09:17:41.000Z","size":3298,"stargazers_count":52,"open_issues_count":6,"forks_count":16,"subscribers_count":6,"default_branch":"master","last_synced_at":"2023-09-19T02:49:05.137Z","etag":null,"topics":["certificate","erlang","letsencrypt","ssl"],"latest_commit_sha":null,"homepage":null,"language":"Erlang","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gbour.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-12-08T17:49:00.000Z","updated_at":"2023-04-21T16:42:14.000Z","dependencies_parsed_at":"2022-09-08T05:11:39.722Z","dependency_job_id":null,"html_url":"https://github.com/gbour/letsencrypt-erlang","commit_stats":null,"previous_names":[],"tags_count":10,"template":null,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gbour%2Fletsencrypt-erlang","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gbour%2Fletsencrypt-erlang/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gbour%2Fletsencrypt-erlang/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gbour%2Fletsencrypt-erlang/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gbour","download_url":"https://codeload.github.com/gbour/letsencrypt-erlang/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243836015,"owners_count":20355615,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["certificate","erlang","letsencrypt","ssl"],"created_at":"2024-10-12T23:27:01.424Z","updated_at":"2025-10-27T09:52:24.450Z","avatar_url":"https://github.com/gbour.png","language":"Erlang","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Build Status](https://travis-ci.org/gbour/letsencrypt-erlang.svg?branch=master)](https://travis-ci.org/gbour/letsencrypt-erlang)\n[![Hex.pm](https://img.shields.io/hexpm/v/letsencrypt.svg)](https://hex.pm/packages/letsencrypt)\n\n# letsencrypt-erlang\nLet's Encrypt client library for Erlang\n\n## Overview\n\nFeatures:\n\n- [x] ACME v2\n- [ ] registering client (with email)\n- [x] issuing RSA certificate\n- [ ] revoking certificate\n- [~] SAN certificate (supplementary domain names)\n- [ ] allow EC keys\n- [ ] choose RSA key length\n- [x] unittests\n- [x] hex package\n\nModes\n- [x] webroot\n- [x] slave\n- [x] standalone (with http server)\n\nValidation challenges\n- [x] http-01 (http)\n- [ ] dns-01\n- [ ] proof-of-possession-01\n\n## Prerequisites\n- openssl \u003e= 1.1.1 (required to generate RSA key and certificate request)\n- erlang OTP (tested with 22.2 version, probably works with older versions as well)\n\n## Building\n\n```\n $\u003e ./rebar3 update\n $\u003e ./rebar3 compile\n```\n\n## Quickstart\n\nYou must execute this example on the server targeted by _mydomain.tld_. \nPort 80 (http) must be opened and a webserver listening on it (line 1) and serving **/path/to/webroot/**\ncontent.  \nBoth **/path/to/webroot** and **/path/to/certs** MUST be writtable by the erlang process\n\n```erlang\n\n $\u003e $(cd /path/to/webroot \u0026\u0026 python -m SimpleHTTPServer 80)\u0026\n $\u003e ./rebar3 shell\n $erl\u003e application:ensure_all_started(letsencrypt).\n $erl\u003e letsencrypt:start([{mode,webroot},{webroot_path,\"/path/to/webroot\"},{cert_path,\"/path/to/certs\"}]).\n $erl\u003e letsencrypt:make_cert(\u003c\u003c\"mydomain.tld\"\u003e\u003e, #{async =\u003e false}).\n{ok, #{cert =\u003e \u003c\u003c\"/path/to/certs/mydomain.tld.crt\"\u003e\u003e, key =\u003e \u003c\u003c\"/path/to/certs/mydomain.tld.key\"\u003e\u003e}}\n $erl\u003e ^C\n\n $\u003e ls -1 /path/to/certs\n letsencrypt.key\n mydomain.tld.crt\n mydomain.tld.csr\n mydomain.tld.key\n```\n\n**Explanations**:\n\n  During the certification process, letsencrypt server returns a challenge and then tries to query the challenge\n  file from the domain name asked to be certified.\n  So letsencrypt-erlang is writing challenge file under **/path/to/webroot** directory.\n  Finally, keys and certificates are written in **/path/to/certs** directory.\n\n## Escript\n\n**bin/eletsencrypt** escript allows certificates management without any lines of Erlang.\nConfiguration is defined in etc/eletsencrypt.yml\n\nOptions:\n * **-h|--help**: show help\n * **-l|--list**: list certificates informations\n   * **-s|--short**: along with *-l*, display informations in short form\n * **-r|--renew**: renew expired certificates\n * **-f|--force**: along with *-r*, force certificates renewal even if not expired\n * **-c|--config CONFIG-FILE**: use *CONFIG-FILE* configuration instead of default one\n\nOptionally, you can provide the domain you want to apply options as parameter\n\n\n## API\nNOTE: if _optional_ is not written, parameter is required\n\n* **letsencrypt:start(Params) :: starts letsencrypt client process**:\nParams is a list of parameters, choose from the followings:\n  * **staging** (optional): use staging API (generating fake certificates - default behavior is to use real API)\n  * **{mode, Mode}**: choose running mode, where **Mode** is one of **webroot**, **slave** or\n    **standalone**\n  * **{cert_path, Path}**: pinpoint path to store generated certificates.\n    Must be writable by erlang process\n  * **{http_timeout, Timeout}** (integer, optional, default to 30000): http queries timeout\n    (in milliseconds)  \n  * **{connect_timeout, Timeout}** is **deprecated**, replaced by **http_timeout**\n\n  \n  Mode-specific parameters:\n  * _webroot_ mode:\n    * **{webroot_path, Path}**: pinpoint path to store challenge thumbprints.\n      Must be writable by erlang process, and available through your webserver as root path\n\n  * _standalone_ mode:\n    * **{port, Port}** (optional, default to *80*): tcp port to listen for http query for\n      challenge validation\n\n  returns:\n    * **{ok, Pid}** with Pid the server process pid\n\n* **letsencrypt:make_cert(Domain, Opts) :: generate a new certificate for the considered domain name**:\n  * **Domain**: domain name (string or binary)\n  * **Opts**: options map\n    * **async** = true|false (optional, _true_ by default): \n    * **callback** (optional, used only when _async=true_): function called once certificate has been\n      generated.\n    * **san** (list(binary), optional): supplementary domain names added to the certificate. \n      **san is not available currently, will be reimplemented soon**.\n    * **challenge** (optional): 'http-01' (default)\n\n  returns:\n    * in asynchronous mode, function returns **async**\n    * in synchronous mode, or as asynchronous callback function parameter:  \n      * **{ok, #{cert =\u003e \u003c\u003c\"/path/to/cert\"\u003e\u003e, key =\u003e \u003c\u003c\"/path/to/key\"\u003e\u003e}}** on success  \n      * **{error, Message}** on error\n\n  examples:\n    * sync mode (shell is locked several seconds waiting result)\n  ```erlang\n    \u003e letsencrypt:make_cert(\u003c\u003c\"mydomain.tld\"\u003e\u003e, #{async =\u003e false}).\n    {ok, #{cert =\u003e \u003c\u003c\"/path/to/cert\"\u003e\u003e, key =\u003e \u003c\u003c\"/path/to/key\"\u003e\u003e}}\n\n    \u003e % domain tld is incorrect\n    \u003e letsencrypt:make_cert(\u003c\u003c\"invalid.tld\"\u003e\u003e, #{async =\u003e false}).\n    {error, \u003c\u003c\"Error creating new authz :: Name does not end in a public suffix\"\u003e\u003e}\n\n    \u003e % domain web server does not return challenge file (ie 404 error)\n    \u003e letsencrypt:make_cert(\u003c\u003c\"example.com\"\u003e\u003e, #{async =\u003e false}).\n    {error, \u003c\u003c\"Invalid response from http://example.com/.well-known/acme-challenge/Bt\"...\u003e\u003e}\n\n    \u003e % returned challenge is wrong\n    \u003e letsencrypt:make_cert(\u003c\u003c\"example.com\"\u003e\u003e, #{async =\u003e false}).\n    {error,\u003c\u003c\"Error parsing key authorization file: Invalid key authorization: 1 parts\"\u003e\u003e}\n    or\n    {error,\u003c\u003c\"Error parsing key authorization file: Invalid key authorization: malformed token\"\u003e\u003e}\n    or\n    {error,\u003c\u003c\"The key authorization file from the server did not match this challenge\"...\u003e\u003e\u003e}\n  ```\n    * async mode ('async' is written immediately)\n  ```erlang\n    \u003e F = fun({Status, Result}) -\u003e io:format(\"completed: ~p (result= ~p)~n\") end.\n    \u003e letsencrypt:make_cert(\u003c\u003c\"example.com\"\u003e\u003e, #{async =\u003e true, callback =\u003e F}).\n    async\n    \u003e\n    ...\n    completed: ok (result= #{cert =\u003e \u003c\u003c\"/path/to/cert\"\u003e\u003e, key =\u003e \u003c\u003c\"/path/to/key\"\u003e\u003e})\n  ```\n\n    * SAN (**not available currently**)\n  ```erlang\n    \u003e letsencrypt:make_cert(\u003c\u003c\"example.com\"\u003e\u003e, #{async =\u003e false, san =\u003e [\u003c\u003c\"www.example.com\"\u003e\u003e]}).\n    {ok, #{cert =\u003e \u003c\u003c\"/path/to/cert\"\u003e\u003e, key =\u003e \u003c\u003c\"/path/to/key\"\u003e\u003e}}\n  ```\n\n    * explicit **'http-01'** challenge\n  ```erlang\n    \u003e letsencrypt:make_cert(\u003c\u003c\"example.com\"\u003e\u003e, #{async =\u003e false, challenge =\u003e 'http-01'}).\n    {ok, #{cert =\u003e \u003c\u003c\"/path/to/cert\"\u003e\u003e, key =\u003e \u003c\u003c\"/path/to/key\"\u003e\u003e}}\n  ```\n\n\n## Action modes\n\n### webroot\n\n*When you're running a webserver (ie apache or nginx) listening on public http port*.\n\n```erlang\non_complete({State, Data}) -\u003e\n    io:format(\"letsencrypt certicate issued: ~p (data: ~p)~n\", [State, Data]),\n    case State of\n        ok -\u003e\n            io:format(\"reloading nginx...~n\"),\n            os:cmd(\"sudo systemctl reload nginx\");\n\n        _  -\u003e pass\n    end.\n\nmain() -\u003e\n    letsencrypt:start([{mode,webroot}, staging, {cert_path,\"/path/to/certs\"}, {webroot_path, \"/var/www/html\"]),\n    letsencrypt:make_cert(\u003c\u003c\"mydomain.tld\"\u003e\u003e, #{callback =\u003e fun on_complete/1}),\n\n    ok.\n```\n\n### slave\n\n*When your erlang application is already running an erlang http server, listening on public http port (ie cowboy)*.\n\n```erlang\n\non_complete({State, Data}) -\u003e\n    io:format(\"letsencrypt certificate issued: ~p (data: ~p)~n\", [State, Data]).\n\nmain() -\u003e\n    Dispatch = cowboy_router:compile([\n        {'_', [\n            {\u003c\u003c\"/.well-known/acme-challenge/:token\"\u003e\u003e, my_letsencrypt_cowboy_handler, []}\n        ]}\n    ]),\n    {ok, _} = cowboy:start_http(my_http_listener, 1, [{port, 80}],\n        [{env, [{dispatch, Dispatch}]}]\n    ),\n\n    letsencrypt:start([{mode,slave}, staging, {cert_path,\"/path/to/certs\"}]),\n    letsencrypt:make_cert(\u003c\u003c\"mydomain.tld\"\u003e\u003e, #{callback =\u003e fun on_complete/1}),\n\n    ok.\n```\n\nmy_letsencrypt_cowboy_handler.erl contains the code to returns letsencrypt thumbprint matching received token\n\n```erlang\n-module(my_letsencrypt_cowboy_handler).\n\n-export([init/3, handle/2, terminate/3]).\n\n\ninit(_, Req, []) -\u003e\n    {Host,_} = cowboy_req:host(Req),\n\n    % NOTES\n    %   - cowboy_req:binding() returns undefined is token not set in URI\n    %   - letsencrypt:get_challenge() returns 'error' if token+thumbprint are not available\n    %\n    Thumbprints = letsencrypt:get_challenge(),\n    {Token,_}   = cowboy_req:binding(token, Req),\n\n    {ok, Req2} = case maps:get(Token, Thumprints, undefined) of\n        Thumbprint -\u003e\n            cowboy_req:reply(200, [{\u003c\u003c\"content-type\"\u003e\u003e, \u003c\u003c\"text/plain\"\u003e\u003e}], Thumbprint, Req);\n\n        _X     -\u003e\n            cowboy_req:reply(404, Req)\n    end,\n\n    {ok, Req2, no_state}.\n\nhandle(Req, State) -\u003e\n    {ok, Req, State}.\n\nterminate(Reason, Req, State) -\u003e\n    ok.\n```\n\n### standalone\n\n*When you have no live http server running on your server*.  \n\nletsencrypt-erlang will start its own webserver just enough time to validate the challenge, then will\nstop it immediately after that.\n\n```erlang\n\non_complete({State, Data}) -\u003e\n    io:format(\"letsencrypt certificate issued: ~p (data: ~p)~n\", [State, Data]).\n\nmain() -\u003e\n    letsencrypt:start([{mode,standalone}, staging, {cert_path,\"/path/to/certs\"}, {port, 80)]),\n    letsencrypt:make_cert(\u003c\u003c\"mydomain.tld\"\u003e\u003e, #{callback =\u003e fun on_complete/1}),\n\n    ok.\n```\n\n## License\n\nletsencrypt-erlang is distributed under APACHE 2.0 license.\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgbour%2Fletsencrypt-erlang","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgbour%2Fletsencrypt-erlang","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgbour%2Fletsencrypt-erlang/lists"}