├── .ocamlformat ├── ca_store.ml ├── download_pem.sh ├── .gitignore ├── ca_store.mli ├── dune ├── CHANGES.md ├── decode_test.ml ├── dune-project ├── castore.opam ├── LICENSE.md ├── flake.nix ├── README.md ├── .github └── workflows │ ├── main.yml │ └── cert.yml ├── flake.lock └── regen.ml /.ocamlformat: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ca_store.ml: -------------------------------------------------------------------------------- 1 | include Pem 2 | include Certificates 3 | -------------------------------------------------------------------------------- /download_pem.sh: -------------------------------------------------------------------------------- 1 | wget https://curl.se/ca/cacert.pem -O cacert.pem 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | # nix ignores 3 | .direnv 4 | result 5 | .envrc 6 | -------------------------------------------------------------------------------- /ca_store.mli: -------------------------------------------------------------------------------- 1 | val pem : string 2 | (** The Mozilla CA certificate store in PEM format. *) 3 | 4 | val certificates : string list 5 | (** The Mozilla CA certificate list ready to be used with libraries like [X509]. *) 6 | -------------------------------------------------------------------------------- /dune: -------------------------------------------------------------------------------- 1 | (mdx 2 | (libraries x509 castore)) 3 | 4 | (library 5 | (public_name castore) 6 | (modules ca_store pem certificates) 7 | (name ca_store)) 8 | 9 | (executable 10 | (name regen) 11 | (modules regen) 12 | (libraries ca-certs x509 cstruct ptime ptime.clock.os)) 13 | 14 | (test 15 | (name decode_test) 16 | (modules decode_test) 17 | (libraries castore cstruct x509)) 18 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## 0.0.2 4 | 5 | This release pre-processes the .pem file into a list of pem certificate strings 6 | that can be easily parsed with `X509` to build chains of trust. 7 | 8 | You can access the whole pem file on `Ca_store.pem` and the individual 9 | certificates in the `Ca_store.certificates` list. 10 | 11 | ## 0.0.1 12 | 13 | Initial release of CAStore including a PEM file generated on Dec 12. 14 | -------------------------------------------------------------------------------- /decode_test.ml: -------------------------------------------------------------------------------- 1 | let time () = None in 2 | (* $MDX part-begin=main *) 3 | let decode_pem ca = 4 | let ca = Cstruct.of_string ca in 5 | let cert = X509.Certificate.decode_pem ca in 6 | Result.get_ok cert 7 | in 8 | let cas = List.map decode_pem Ca_store.certificates in 9 | let authenticator = X509.Authenticator.chain_of_trust ~time cas in 10 | (* ... *) 11 | (* $MDX part-end *) 12 | ignore authenticator; 13 | 14 | print_endline "decode_test: OK" 15 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.11) 2 | (using mdx 0.4) 3 | 4 | (name castore) 5 | 6 | (generate_opam_files true) 7 | 8 | (source (github leostera/castore)) 9 | 10 | (authors "Leandro Ostera ") 11 | 12 | (maintainers "Leandro Ostera ") 13 | 14 | (license MIT) 15 | 16 | (package 17 | (name castore) 18 | (synopsis "A portable CA Store with a global .crt and .pem files") 19 | (depends 20 | (ocaml (>="5.1")) 21 | (mdx (and :with-test (>= "2.3.1"))) 22 | (x509 (and :with-doc (>= "0.16.5"))) 23 | dune) 24 | (tags (https tls cert crt pem ca "ca store"))) 25 | -------------------------------------------------------------------------------- /castore.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "A portable CA Store with a global .crt and .pem files" 4 | maintainer: ["Leandro Ostera "] 5 | authors: ["Leandro Ostera "] 6 | license: "MIT" 7 | tags: ["https" "tls" "cert" "crt" "pem" "ca" "ca store"] 8 | homepage: "https://github.com/leostera/castore" 9 | bug-reports: "https://github.com/leostera/castore/issues" 10 | depends: [ 11 | "ocaml" {>= "5.1"} 12 | "mdx" {with-test & >= "2.3.1"} 13 | "x509" {with-doc & >= "0.16.5"} 14 | "dune" {>= "3.11"} 15 | "odoc" {with-doc} 16 | ] 17 | build: [ 18 | ["dune" "subst"] {dev} 19 | [ 20 | "dune" 21 | "build" 22 | "-p" 23 | name 24 | "-j" 25 | jobs 26 | "@install" 27 | "@runtest" {with-test} 28 | "@doc" {with-doc} 29 | ] 30 | ] 31 | dev-repo: "git+https://github.com/leostera/castore.git" 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023, Leandro Ostera 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A portable pure OCaml CA Store"; 3 | 4 | inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 5 | 6 | outputs = inputs@{ flake-parts, ... }: 7 | flake-parts.lib.mkFlake { inherit inputs; } { 8 | systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ]; 9 | perSystem = { config, self', inputs', pkgs, system, ... }: 10 | let 11 | inherit (pkgs) ocamlPackages mkShell; 12 | inherit (ocamlPackages) buildDunePackage; 13 | name = "castore"; 14 | version = "0.0.2"; 15 | in 16 | { 17 | devShells = { 18 | default = mkShell { 19 | buildInputs = [ ocamlPackages.utop ]; 20 | inputsFrom = [ self'.packages.default ]; 21 | }; 22 | }; 23 | 24 | packages = { 25 | default = buildDunePackage { 26 | inherit version; 27 | pname = name; 28 | propagatedBuildInputs = with ocamlPackages; [ 29 | (mdx.override { 30 | inherit logs; 31 | }) 32 | x509 33 | ]; 34 | src = ./.; 35 | }; 36 | }; 37 | }; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CAStore 🦫 2 | 3 | A portable pure OCaml CA Store, with no dependencies, inspired by Elixir's 4 | [:castore][castore]. 5 | 6 | [castore]: https://github.com/elixir-mint/castore 7 | 8 | ## Getting Started 9 | 10 | First, install `castore` in your switch: 11 | 12 | ```zsh 13 | # latest published version 14 | opam install castore 15 | 16 | # latest development version 17 | opam pin castore git+https://github.com/leostera/castore 18 | ``` 19 | 20 | Now we can add it to your dune project dependencies: 21 | 22 | ``` 23 | (package 24 | ;... 25 | (depends 26 | (castore (>= "0.0.0")) 27 | ;...) 28 | ;...) 29 | ``` 30 | 31 | And to your dune stanzas: 32 | 33 | ``` 34 | (executable 35 | (name my_app) 36 | (libraries castore)) 37 | ``` 38 | 39 | And finally we can use it by decoding the certificates, and building a chain of 40 | trust we can build our Tls config with. 41 | 42 | Here's an example of how to do it: 43 | 44 | 45 | ```ocaml 46 | let decode_pem ca = 47 | let ca = Cstruct.of_string ca in 48 | let cert = X509.Certificate.decode_pem ca in 49 | Result.get_ok cert 50 | in 51 | let cas = List.map decode_pem Ca_store.certificates in 52 | let authenticator = X509.Authenticator.chain_of_trust ~time cas in 53 | (* ... *) 54 | ``` 55 | 56 | ## Acknowledgements 57 | 58 | This project would not be possible without `ocaml-tls` and `ca-certs`, in fact, 59 | we use `ca-certs` to generate the `Ca_store.cas` with code taken from the 60 | implementation of `ca-certs`. 61 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | schedule: 7 | # Prime the caches every Monday 8 | - cron: 0 1 * * MON 9 | 10 | permissions: read-all 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | os: 18 | - macos-latest 19 | - ubuntu-latest 20 | ocaml-compiler: 21 | - "5.1" 22 | allow-prerelease-opam: 23 | - true 24 | opam-repositories: 25 | - |- 26 | default: https://github.com/ocaml/opam-repository.git 27 | # include: 28 | # - os: windows-latest 29 | # ocaml-compiler: ocaml-variants.5.1.0+options,ocaml-option-mingw 30 | # allow-prerelease-opam: false 31 | # opam-repositories: |- 32 | # windows-5.0: https://github.com/dra27/opam-repository.git#windows-5.0 33 | # sunset: https://github.com/ocaml-opam/opam-repository-mingw.git#sunset 34 | # default: https://github.com/ocaml/opam-repository.git 35 | 36 | runs-on: ${{ matrix.os }} 37 | 38 | steps: 39 | - name: Checkout tree 40 | uses: actions/checkout@v4 41 | 42 | - name: Set-up OCaml 43 | uses: ocaml/setup-ocaml@v2 44 | with: 45 | ocaml-compiler: ${{ matrix.ocaml-compiler }} 46 | allow-prerelease-opam: ${{ matrix.allow-prerelease-opam }} 47 | opam-repositories: ${{ matrix.opam-repositories }} 48 | 49 | - run: opam install . --deps-only --with-test 50 | 51 | - run: opam exec -- dune build 52 | 53 | - run: opam exec -- dune test 54 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-parts": { 4 | "inputs": { 5 | "nixpkgs-lib": "nixpkgs-lib" 6 | }, 7 | "locked": { 8 | "lastModified": 1706830856, 9 | "narHash": "sha256-a0NYyp+h9hlb7ddVz4LUn1vT/PLwqfrWYcHMvFB1xYg=", 10 | "owner": "hercules-ci", 11 | "repo": "flake-parts", 12 | "rev": "b253292d9c0a5ead9bc98c4e9a26c6312e27d69f", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "id": "flake-parts", 17 | "type": "indirect" 18 | } 19 | }, 20 | "nixpkgs": { 21 | "locked": { 22 | "lastModified": 1708807242, 23 | "narHash": "sha256-sRTRkhMD4delO/hPxxi+XwLqPn8BuUq6nnj4JqLwOu0=", 24 | "owner": "NixOS", 25 | "repo": "nixpkgs", 26 | "rev": "73de017ef2d18a04ac4bfd0c02650007ccb31c2a", 27 | "type": "github" 28 | }, 29 | "original": { 30 | "owner": "NixOS", 31 | "ref": "nixos-unstable", 32 | "repo": "nixpkgs", 33 | "type": "github" 34 | } 35 | }, 36 | "nixpkgs-lib": { 37 | "locked": { 38 | "dir": "lib", 39 | "lastModified": 1706550542, 40 | "narHash": "sha256-UcsnCG6wx++23yeER4Hg18CXWbgNpqNXcHIo5/1Y+hc=", 41 | "owner": "NixOS", 42 | "repo": "nixpkgs", 43 | "rev": "97b17f32362e475016f942bbdfda4a4a72a8a652", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "dir": "lib", 48 | "owner": "NixOS", 49 | "ref": "nixos-unstable", 50 | "repo": "nixpkgs", 51 | "type": "github" 52 | } 53 | }, 54 | "root": { 55 | "inputs": { 56 | "flake-parts": "flake-parts", 57 | "nixpkgs": "nixpkgs" 58 | } 59 | } 60 | }, 61 | "root": "root", 62 | "version": 7 63 | } 64 | -------------------------------------------------------------------------------- /.github/workflows/cert.yml: -------------------------------------------------------------------------------- 1 | name: Update Certificate 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | # Run every Wednesday at midnight 7 | - cron: 0 0 * * WED 8 | 9 | jobs: 10 | update-cert: 11 | runs-on: ubuntu-latest 12 | env: 13 | ocaml-compiler: "5.1" 14 | steps: 15 | - name: Checkout tree 16 | uses: actions/checkout@v4 17 | 18 | - name: Download and compare latest certificate 19 | id: check_changes 20 | continue-on-error: true 21 | shell: bash 22 | run: | 23 | current=$(cat cacert.pem | sha256sum) 24 | ./download_pem.sh 25 | latest=$(cat cacert.pem | sha256sum) 26 | if [ "$current" = "$latest" ]; then 27 | echo "No changes to cacert.pem. Skipping build!" 28 | exit 1 29 | fi 30 | 31 | - name: Set-up OCaml 32 | uses: ocaml/setup-ocaml@v2 33 | if: steps.check_changes.outcome == 'success' 34 | with: 35 | ocaml-compiler: ${{ env.ocaml-compiler }} 36 | 37 | - name: Install OCaml deps 38 | if: steps.check_changes.outcome == 'success' 39 | run: | 40 | opam install castore x509 ca-certs cstruct mdx ocamlformat dune-release 41 | opam exec -- dune build 42 | ./_build/default/regen.exe 43 | opam exec -- dune fmt || true 44 | 45 | - name: Bump version and create release 46 | if: steps.check_changes.outcome == 'success' 47 | shell: bash 48 | run: | 49 | git pull --tags --force 50 | current_version=$(git describe --tags $(git rev-list --tags --max-count=1)) 51 | IFS='.' read -r major minor patch <<< "$current_version" 52 | new_version="$major.$minor.$((patch + 1))" 53 | 54 | opam exec -- dune-release tag "$new_version" 55 | opam exec -- dune-release distrib 56 | opam exec -- dune-release publish distrib -y 57 | opam exec -- dune-release opam pkg 58 | opam exec -- dune-release opam submit 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | -------------------------------------------------------------------------------- /regen.ml: -------------------------------------------------------------------------------- 1 | let pem = 2 | let ic = open_in "cacert.pem" in 3 | let pem = In_channel.input_all ic in 4 | close_in ic; 5 | pem 6 | 7 | let generate_pem pem = 8 | let file = Format.sprintf "let pem = {|%s|}" pem in 9 | let oc = open_out "pem.ml" in 10 | Out_channel.output_string oc file; 11 | Printf.printf "🔑 Generated pem.ml\n"; 12 | close_out oc 13 | 14 | (****************************************************************************** 15 | 16 | The X509 and Auth modules below were ported from `ca-certs`, specifically 17 | from: 18 | 19 | * https://github.com/mirage/ca-certs/blob/main/lib/ca_certs.ml 20 | 21 | under this license: 22 | 23 | Copyright (c) 2014, David Kaloper and Hannes Mehnert 24 | All rights reserved. 25 | 26 | Redistribution and use in source and binary forms, with or without modification, 27 | are permitted provided that the following conditions are met: 28 | 29 | * Redistributions of source code must retain the above copyright notice, this 30 | list of conditions and the following disclaimer. 31 | 32 | * Redistributions in binary form must reproduce the above copyright notice, this 33 | list of conditions and the following disclaimer in the documentation and/or 34 | other materials provided with the distribution. 35 | 36 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 37 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 38 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 39 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 40 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 41 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 42 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 43 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 44 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 45 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 46 | 47 | *******************************************************************************) 48 | let generate_cas pem = 49 | let d = "-----" in 50 | let new_cert = d ^ "BEGIN CERTIFICATE" ^ d 51 | and end_of_cert = d ^ "END CERTIFICATE" ^ d in 52 | let len_new = String.length new_cert 53 | and len_end = String.length end_of_cert in 54 | let lines = String.split_on_char '\n' pem in 55 | let it, cas = 56 | List.fold_left 57 | (fun (acc, cas) line -> 58 | match acc with 59 | | None 60 | when String.length line >= len_new 61 | && String.(equal (sub line 0 len_new) new_cert) -> 62 | (Some [ line ], cas) 63 | | None -> (None, cas) 64 | | Some lines 65 | when String.length line >= len_end 66 | && String.(equal (sub line 0 len_end) end_of_cert) -> ( 67 | let data = String.concat "\n" (List.rev (line :: lines)) in 68 | match X509.Certificate.decode_pem (Cstruct.of_string data) with 69 | | Ok _ca -> (None, data :: cas) 70 | | Error (`Msg msg) -> 71 | Printf.printf "Failed to decode a trust anchor %s.\n" msg; 72 | Printf.printf "Full certificate:@.%s\n" data; 73 | (None, cas)) 74 | | Some lines -> (Some (line :: lines), cas)) 75 | (None, []) lines 76 | in 77 | (match it with 78 | | None -> () 79 | | Some lines -> 80 | Printf.printf "ignoring leftover data: %s\n" 81 | (String.concat "\n" (List.rev lines))); 82 | 83 | let cas = List.rev cas in 84 | let cas = 85 | String.concat ";\n" (List.map (fun ca -> Format.asprintf "{|%s|}" ca) cas) 86 | in 87 | let file = Format.sprintf "let certificates = [\n%s\n] " cas in 88 | let oc = open_out "certificates.ml" in 89 | Out_channel.output_string oc file; 90 | Printf.printf "🔒 Generated certificates.ml\n"; 91 | close_out oc 92 | 93 | let () = 94 | Printf.printf "Regenerating modules:\n"; 95 | generate_pem pem; 96 | generate_cas pem; 97 | Printf.printf "OK\n" 98 | --------------------------------------------------------------------------------