1{ lib
2, stdenv
3, writeText
4, fetchurl
5, buildcatrust
6, blacklist ? []
7, extraCertificateFiles ? []
8, extraCertificateStrings ? []
9
10# Used by update.sh
11, nssOverride ? null
12
13# Used for tests only
14, runCommand
15, cacert
16, openssl
17}:
18
19let
20 blocklist = writeText "cacert-blocklist.txt" (lib.concatStringsSep "\n" (blacklist ++ [
21 # Mozilla does not trust new certificates issued by these CAs after 2022/11/30¹
22 # in their products, but unfortunately we don't have such a fine-grained
23 # solution for most system packages², so we decided to eject these.
24 #
25 # [1] https://groups.google.com/a/mozilla.org/g/dev-security-policy/c/oxX69KFvsm4/m/yLohoVqtCgAJ
26 # [2] https://utcc.utoronto.ca/~cks/space/blog/linux/CARootStoreTrustProblem
27 "TrustCor ECA-1"
28 "TrustCor RootCert CA-1"
29 "TrustCor RootCert CA-2"
30 ]));
31 extraCertificatesBundle = writeText "cacert-extra-certificates-bundle.crt" (lib.concatStringsSep "\n\n" extraCertificateStrings);
32
33 srcVersion = "3.89.1";
34 version = if nssOverride != null then nssOverride.version else srcVersion;
35 meta = with lib; {
36 homepage = "https://curl.haxx.se/docs/caextract.html";
37 description = "A bundle of X.509 certificates of public Certificate Authorities (CA)";
38 platforms = platforms.all;
39 maintainers = with maintainers; [ fpletz lukegb ];
40 license = licenses.mpl20;
41 };
42 certdata = stdenv.mkDerivation {
43 pname = "nss-cacert-certdata";
44 inherit version;
45
46 src = if nssOverride != null then nssOverride.src else fetchurl {
47 url = "mirror://mozilla/security/nss/releases/NSS_${lib.replaceStrings ["."] ["_"] version}_RTM/src/nss-${version}.tar.gz";
48 hash = "sha256-OtrtuecMPF9AYDv2CgHjNhkKbb4Bkp05XxawH+hKAVY=";
49 };
50
51 dontBuild = true;
52
53 installPhase = ''
54 runHook preInstall
55
56 mkdir $out
57 cp nss/lib/ckfw/builtins/certdata.txt $out
58
59 runHook postInstall
60 '';
61
62 inherit meta;
63 };
64in
65stdenv.mkDerivation rec {
66 pname = "nss-cacert";
67 inherit version;
68
69 src = certdata;
70
71 outputs = [ "out" "unbundled" "p11kit" ];
72
73 nativeBuildInputs = [ buildcatrust ];
74
75 buildPhase = ''
76 mkdir unbundled
77 buildcatrust \
78 --certdata_input certdata.txt \
79 --ca_bundle_input "${extraCertificatesBundle}" ${lib.escapeShellArgs (map (arg: "${arg}") extraCertificateFiles)} \
80 --blocklist "${blocklist}" \
81 --ca_bundle_output ca-bundle.crt \
82 --ca_unpacked_output unbundled \
83 --p11kit_output ca-bundle.trust.p11-kit
84 '';
85
86 installPhase = ''
87 install -D -t "$out/etc/ssl/certs" ca-bundle.crt
88
89 # install p11-kit specific output to p11kit output
90 install -D -t "$p11kit/etc/ssl/trust-source" ca-bundle.trust.p11-kit
91
92 # install individual certs in unbundled output
93 install -D -t "$unbundled/etc/ssl/certs" unbundled/*.crt
94 '';
95
96 setupHook = ./setup-hook.sh;
97
98 passthru = {
99 updateScript = ./update.sh;
100 tests = let
101 isTrusted = ''
102 isTrusted() {
103 # isTrusted <unbundled-dir> <ca name> <ca sha256 fingerprint>
104 for f in $1/etc/ssl/certs/*.crt; do
105 if ! [[ -s "$f" ]]; then continue; fi
106 fingerprint="$(openssl x509 -in "$f" -noout -fingerprint -sha256 | cut -f2 -d=)"
107 if [[ "x$fingerprint" == "x$3" ]]; then
108 # If the certificate is treated as rejected for TLS Web Server, then we consider it untrusted.
109 if openssl x509 -in "$f" -noout -text | grep -q '^Rejected Uses:'; then
110 if openssl x509 -in "$f" -noout -text | grep -A1 '^Rejected Uses:' | grep -q 'TLS Web Server'; then
111 return 1
112 fi
113 fi
114 return 0
115 fi
116 done
117 return 1
118 }
119 '';
120 in {
121 # Test that building this derivation with a blacklist works, and that UTF-8 is supported.
122 blacklist-utf8 = let
123 blacklistCAToFingerprint = {
124 # "blacklist" uses the CA name from the NSS bundle, but we check for presence using the SHA256 fingerprint.
125 "CFCA EV ROOT" = "5C:C3:D7:8E:4E:1D:5E:45:54:7A:04:E6:87:3E:64:F9:0C:F9:53:6D:1C:CC:2E:F8:00:F3:55:C4:C5:FD:70:FD";
126 "NetLock Arany (Class Gold) Főtanúsítvány" = "6C:61:DA:C3:A2:DE:F0:31:50:6B:E0:36:D2:A6:FE:40:19:94:FB:D1:3D:F9:C8:D4:66:59:92:74:C4:46:EC:98";
127 };
128 mapBlacklist = f: lib.concatStringsSep "\n" (lib.mapAttrsToList f blacklistCAToFingerprint);
129 in runCommand "verify-the-cacert-filter-output" {
130 cacert = cacert.unbundled;
131 cacertWithExcludes = (cacert.override {
132 blacklist = builtins.attrNames blacklistCAToFingerprint;
133 }).unbundled;
134
135 nativeBuildInputs = [ openssl ];
136 } ''
137 ${isTrusted}
138
139 # Ensure that each certificate is in the main "cacert".
140 ${mapBlacklist (caName: caFingerprint: ''
141 isTrusted "$cacert" "${caName}" "${caFingerprint}" || ({
142 echo "CA fingerprint ${caFingerprint} (${caName}) is missing from the CA bundle. Consider picking a different CA for the blacklist test." >&2
143 exit 1
144 })
145 '')}
146
147 # Ensure that each certificate is NOT in the "cacertWithExcludes".
148 ${mapBlacklist (caName: caFingerprint: ''
149 isTrusted "$cacertWithExcludes" "${caName}" "${caFingerprint}" && ({
150 echo "CA fingerprint ${caFingerprint} (${caName}) is present in the cacertWithExcludes bundle." >&2
151 exit 1
152 })
153 '')}
154
155 touch "$out"
156 '';
157
158 # Test that we can add additional certificates to the store, and have them be trusted.
159 extra-certificates = let
160 extraCertificateStr = ''
161 -----BEGIN CERTIFICATE-----
162 MIIB5DCCAWqgAwIBAgIUItvsAYEIdYDkOIo5sdDYMcUaNuIwCgYIKoZIzj0EAwIw
163 KTEnMCUGA1UEAwweTml4T1MgY2FjZXJ0IGV4dHJhIGNlcnRpZmljYXRlMB4XDTIx
164 MDYxMjE5MDQzMFoXDTIyMDYxMjE5MDQzMFowKTEnMCUGA1UEAwweTml4T1MgY2Fj
165 ZXJ0IGV4dHJhIGNlcnRpZmljYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEuP8y
166 lAm6ZyQt9v/P6gTlV/a9R+D61WjucW04kaegOhg8csiluimYodiSv0Pbgymu+Zxm
167 A3Bz9QGmytaYTiJ16083rJkwwIhqoYl7kWsLzreSTaLz87KH+rdeol59+H0Oo1Mw
168 UTAdBgNVHQ4EFgQUCxuHfvqI4YVU5M+A0+aKvd1LrdswHwYDVR0jBBgwFoAUCxuH
169 fvqI4YVU5M+A0+aKvd1LrdswDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNo
170 ADBlAjEArgxgjdNmRlSEuai0dzlktmBEDZKy2Iiul+ttSoce9ohfEVYESwO602HW
171 keVvI56vAjBCro3dc3m2TuktiKO6lQV56PUEyxko4H/sR5pnHlduCGRDlFzQKXf/
172 pMMmtj7cVb8=
173 -----END CERTIFICATE-----
174 '';
175 extraCertificateFile = ./test-cert-file.crt;
176 extraCertificatesToFingerprint = {
177 # String above
178 "NixOS cacert extra certificate string" = "A3:20:D0:84:96:97:25:FF:98:B8:A9:6D:A3:7C:89:95:6E:7A:77:21:92:F3:33:E9:31:AF:5E:03:CE:A9:E5:EE";
179
180 # File
181 "NixOS cacert extra certificate file" = "88:B8:BE:A7:57:AC:F1:FE:D6:98:8B:50:E0:BD:0A:AE:88:C7:DF:70:26:E1:67:5E:F5:F6:91:27:FF:02:D4:A5";
182 };
183 mapExtra = f: lib.concatStringsSep "\n" (lib.mapAttrsToList f extraCertificatesToFingerprint);
184 in runCommand "verify-the-cacert-extra-output" {
185 cacert = cacert.unbundled;
186 cacertWithExtras = (cacert.override {
187 extraCertificateStrings = [ extraCertificateStr ];
188 extraCertificateFiles = [ extraCertificateFile ];
189 }).unbundled;
190
191 nativeBuildInputs = [ openssl ];
192 } ''
193 ${isTrusted}
194
195 # Ensure that the extra certificate is not in the main "cacert".
196 ${mapExtra (extraName: extraFingerprint: ''
197 isTrusted "$cacert" "${extraName}" "${extraFingerprint}" && ({
198 echo "'extra' CA fingerprint ${extraFingerprint} (${extraName}) is present in the main CA bundle." >&2
199 exit 1
200 })
201 '')}
202
203 # Ensure that the extra certificates ARE in the "cacertWithExtras".
204 ${mapExtra (extraName: extraFingerprint: ''
205 isTrusted "$cacertWithExtras" "${extraName}" "${extraFingerprint}" || ({
206 echo "CA fingerprint ${extraFingerprint} (${extraName}) is not present in the cacertWithExtras bundle." >&2
207 exit 1
208 })
209 '')}
210
211 touch "$out"
212 '';
213 };
214 };
215
216 inherit meta;
217}