Trusting CA certificates in Guix System

[GunnarGrop] - - 4 mins read

I’m going to be honest. It took me frankly an embarassing amount of time to figure out how I could trust custom CA certificates in Guix System. I’ve been using it as my main operating system for almost a year at this point, and I only figured this out recently.

The core of my problem was (apart from not being very good at Scheme or Guix) that this wasn’t clearly documented anywhere. Mainstream GNU/Linux distros have a clear way to do this. Just place the PEM encoded files under /usr/local/share/ca-certificates or /etc/pki/trust/anchors, or something similar, then run update-ca-certificates. The certificates will then be added to the system wide trust bundle, usually somewhere under /etc/ssl/certs. Even Nix, which Guix is originally forked from, has a short and very clear article about how to do this.

Guix just has this article: X.509 Certificates. Nowhere does it say how do actually ensure certificates end up in the systems trust bundle. It does mention the nss-certs package, which Guix installs by default to provide CA certs from Mozilla.

Being quite new to Scheme and Guix, I got way to hung up on this package. For some reason I was under the impressions that this package took care of actually generating the trust bundle at /etc/ssl/certs/ca-certificates.crt, which is does not. Turns out, I didn’t need to care about nss-certs at all.

If we look at that file, it should be obvious.

$ ls -la /etc/ssl/certs/ca-certificates.crt
lrwxrwxrwx 1 root root 99  1 jan  1970 /etc/ssl/certs/ca-certificates.crt -> /gnu/store/l3y2wisv02pq1qkh6j101crr8bjs0xzr-ca-certificate-bundle/etc/ssl/certs/ca-certificates.crt

It’s not even part of the nss-certs /gnu/store/ item!

Then I found another package, le-certs, a package for installing root and intermediate CA certs from Let’s Encrypt, and at this point it became quite clear that I’d been an idiot. Turns out, you can just write a package that ensures some certificates files gets placed under /etc/ssl/certs and create the appropriate hash symlinks with c_rehash, then Guix will make sure those certs end up in the system trust bundle.

You can create a package definition anywhere, maybe just in your dotfiles repository, but I opted to create an actual channel since I wanted this package to me installed on both my PC’s, and on my virtual machines (managed via a different central repository).

For reference, here is my Guix channel: https://git.sr.ht/~gunnargrop/guix-channel. And here is the tree output:

.
├── COPYING
├── gunnargrop
│   ├── auxillary
│   │   └── ca-certs
│   │       ├── gunnar-grop-ca.pem
│   │       └── home-internal-ca.pem
│   ├── packages
│   │   └── pki.scm
│   └── services
│       └── virtualization.scm
└── README.md

Here is what (gunnargrop packages pki) looks like:

(define-module (gunnargrop packages pki)
  #:use-module ((guix licenses) #:prefix license:)
  #:use-module (guix build-system trivial)
  #:use-module (guix packages)
  #:use-module (guix gexp)
  #:use-module (guix utils)
  #:use-module (gnu packages tls)
  #:use-module (gnu packages perl))

(define-public grop-certs
  (package
   (name "grop-certs")
   (version "1")
   (source #f)
   (build-system trivial-build-system)
   (inputs
    `(("gunnar-grop-root"
       ,(local-file "../auxillary/ca-certs/gunnar-grop-ca.pem"))
      ("home-internal-root"
       ,(local-file "../auxillary/ca-certs/home-internal-ca.pem"))))
   (native-inputs
    (list openssl perl))
   (arguments
    (list
     #:modules '((guix build utils))
     #:builder
     #~(begin
         (use-modules (guix build utils))

         (let ((out (string-append #$output "/etc/ssl/certs"))
               (openssl
                (assoc-ref %build-inputs "openssl"))
               (perl
                (assoc-ref %build-inputs "perl"))
               (gunnar-grop-root (assoc-ref %build-inputs "gunnar-grop-root"))
               (home-internal-root (assoc-ref %build-inputs "home-internal-root")))

           (mkdir-p out)
           (for-each
            (lambda (cert)
              (copy-file cert (string-append out "/"
                                             (strip-store-file-name cert))))
            (list gunnar-grop-root
                  home-internal-root))

           ;; Create hash symlinks suitable for OpenSSL ('SSL_CERT_DIR' and
           ;; similar.)
           (chdir out)
           (invoke (string-append perl "/bin/perl")
                   (string-append openssl "/bin/c_rehash")
                   ".")))))

   (home-page "https://grop.dev")
   (synopsis "Gunnar Grop CA certificates")
   (description
    "This package provides a certificate store containing my own
root and intermediate CA certificates.")
   (license license:public-domain)))

If you look at the le-certs package you can see that mine is basically a straight copy. The main difference being that my inputs (.pem files) come from local files, instead of being downloaded from the internet.

Anyway, after adding my own channel to /.config/guix/channels.scm I just added grop-certs to my systems packages definition:

(use-modules (gunnargrop packages pki))
; ...
(packages (append (list curl
                        grop-certs)
                  %base-packages))

And voilà! After a system reconfigure my CA certificates are present in the system trust bundle, and I can finally use curl without specifying CA files.

I hope whoever reads this finds it helpful, as I know I struggled with this particular part of Guix for a long time.