├── .gitignore
├── nixos
└── tests
│ ├── nginx
│ └── root
│ │ └── index.html
│ ├── nginx.nix
│ └── nebula.nix
├── flake.lock
├── flake.nix
├── COPYING.md
├── README.md
├── nixpkcs.sh
├── overlay.nix
└── module.nix
/.gitignore:
--------------------------------------------------------------------------------
1 | result*
2 | *.pin
3 |
--------------------------------------------------------------------------------
/nixos/tests/nginx/root/index.html:
--------------------------------------------------------------------------------
1 |
Hello, nixpkcs!
2 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-parts": {
4 | "inputs": {
5 | "nixpkgs-lib": "nixpkgs-lib"
6 | },
7 | "locked": {
8 | "lastModified": 1763759067,
9 | "narHash": "sha256-LlLt2Jo/gMNYAwOgdRQBrsRoOz7BPRkzvNaI/fzXi2Q=",
10 | "owner": "hercules-ci",
11 | "repo": "flake-parts",
12 | "rev": "2cccadc7357c0ba201788ae99c4dfa90728ef5e0",
13 | "type": "github"
14 | },
15 | "original": {
16 | "owner": "hercules-ci",
17 | "repo": "flake-parts",
18 | "type": "github"
19 | }
20 | },
21 | "flakever": {
22 | "locked": {
23 | "lastModified": 1763450705,
24 | "narHash": "sha256-TUSrRfT76OAXty9A4fXlOOfVfJGDglFQs06b8b+f5NY=",
25 | "owner": "numinit",
26 | "repo": "flakever",
27 | "rev": "a69629e4133fbcdf3c7aae477bd6687bb19e0778",
28 | "type": "github"
29 | },
30 | "original": {
31 | "owner": "numinit",
32 | "repo": "flakever",
33 | "type": "github"
34 | }
35 | },
36 | "nixpkgs": {
37 | "locked": {
38 | "lastModified": 1765186076,
39 | "narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=",
40 | "owner": "NixOS",
41 | "repo": "nixpkgs",
42 | "rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8",
43 | "type": "github"
44 | },
45 | "original": {
46 | "owner": "NixOS",
47 | "ref": "nixos-unstable",
48 | "repo": "nixpkgs",
49 | "type": "github"
50 | }
51 | },
52 | "nixpkgs-lib": {
53 | "locked": {
54 | "lastModified": 1761765539,
55 | "narHash": "sha256-b0yj6kfvO8ApcSE+QmA6mUfu8IYG6/uU28OFn4PaC8M=",
56 | "owner": "nix-community",
57 | "repo": "nixpkgs.lib",
58 | "rev": "719359f4562934ae99f5443f20aa06c2ffff91fc",
59 | "type": "github"
60 | },
61 | "original": {
62 | "owner": "nix-community",
63 | "repo": "nixpkgs.lib",
64 | "type": "github"
65 | }
66 | },
67 | "nixpkgs-lib_2": {
68 | "locked": {
69 | "lastModified": 1765070080,
70 | "narHash": "sha256-5D1Mcm2dQ1aPzQ0sbXluHVUHququ8A7PKJd7M3eI9+E=",
71 | "owner": "nix-community",
72 | "repo": "nixpkgs.lib",
73 | "rev": "e0cad9791b0c168931ae562977703b72d9360836",
74 | "type": "github"
75 | },
76 | "original": {
77 | "owner": "nix-community",
78 | "repo": "nixpkgs.lib",
79 | "type": "github"
80 | }
81 | },
82 | "root": {
83 | "inputs": {
84 | "flake-parts": "flake-parts",
85 | "flakever": "flakever",
86 | "nixpkgs": "nixpkgs",
87 | "nixpkgs-lib": "nixpkgs-lib_2"
88 | }
89 | }
90 | },
91 | "root": "root",
92 | "version": 7
93 | }
94 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "Add support for PKCS#11 smartcards to various Nix packages";
3 | inputs = {
4 | flake-parts.url = "github:hercules-ci/flake-parts";
5 | flakever.url = "github:numinit/flakever";
6 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
7 | nixpkgs-lib.url = "github:nix-community/nixpkgs.lib";
8 | };
9 |
10 | outputs =
11 | inputs@{
12 | self,
13 | flake-parts,
14 | flakever,
15 | nixpkgs,
16 | nixpkgs-lib,
17 | ...
18 | }:
19 | let
20 | flakeverConfig = flakever.lib.mkFlakever {
21 | inherit inputs;
22 | digits = [
23 | 1
24 | 2
25 | 1
26 | ];
27 | };
28 |
29 | inherit (nixpkgs-lib) lib;
30 | in
31 | flake-parts.lib.mkFlake { inherit inputs; } {
32 | flake = {
33 | nixosModules.default = import ./module.nix self;
34 | overlays.default = import ./overlay.nix {
35 | inherit self lib;
36 | };
37 | inherit (flakeverConfig) version versionCode;
38 | versionTemplate = "1.3.1-";
39 | };
40 |
41 | systems = [
42 | "x86_64-linux"
43 | "aarch64-linux"
44 | ];
45 |
46 | perSystem =
47 | {
48 | config,
49 | system,
50 | pkgs,
51 | final,
52 | lib,
53 | ...
54 | }:
55 | {
56 | _module.args.pkgs = import inputs.nixpkgs {
57 | inherit system;
58 | overlays = [
59 | self.overlays.default
60 | ];
61 | config = { };
62 | };
63 |
64 | devShells.default =
65 | with pkgs;
66 | mkShell {
67 | name = "nixpkcs-dev";
68 | packages = [ shellcheck ];
69 | };
70 |
71 | checks = {
72 | nssNebulaTest = pkgs.callPackage ./nixos/tests/nebula.nix {
73 | inherit self nixpkgs;
74 | inherit (pkgs.nss_latest) pkcs11Module;
75 | extraKeypairOptions = {
76 | token = "NSS Certificate DB";
77 | slot = 2;
78 | storeInitHook = pkgs.writeShellScript "nss-test-store-init" ''
79 | chown -R nebula-nixpkcs:nebula-nixpkcs "$NIXPKCS_STORE_DIR" || true
80 |
81 | key_options_so_pin_file="$(echo "$NIXPKCS_KEY_SPEC" | jq -r '.keyOptions?.soPinFile? // ""')"
82 | if [ -n "$key_options_so_pin_file" ] && [ -f "$key_options_so_pin_file" ]; then
83 | chown nebula-nixpkcs:nebula-nixpkcs "$key_options_so_pin_file" || true
84 | fi
85 | cert_options_user_pin_file="$(echo "$NIXPKCS_KEY_SPEC" | jq -r '.certOptions?.pinFile? // ""')"
86 | if [ -n "$cert_options_user_pin_file" ] && [ -f "$cert_options_user_pin_file" ]; then
87 | chown nebula-nixpkcs:nebula-nixpkcs "$cert_options_user_pin_file" || true
88 | fi
89 | '';
90 | };
91 | };
92 | tpmNebulaTest = pkgs.callPackage ./nixos/tests/nebula.nix {
93 | inherit self nixpkgs;
94 | inherit (pkgs.tpm2-pkcs11.abrmd) pkcs11Module;
95 | baseKeyId = 256; # swtpm supports rather high IDs, we should test them...
96 | extraKeypairOptions = {
97 | token = "nixpkcs";
98 | };
99 | extraMachineOptions =
100 | { config, ... }:
101 | {
102 | virtualisation.tpm.enable = true;
103 | security.pkcs11.tpm2.enable = true;
104 | users.users."nebula-nixpkcs" = lib.mkIf (config.services.nebula.networks.nixpkcs.enable or false) {
105 | extraGroups = [ "tss" ];
106 | };
107 | };
108 | };
109 | nginxTest = pkgs.callPackage ./nixos/tests/nginx.nix {
110 | inherit self nixpkgs;
111 | inherit (pkgs.tpm2-pkcs11.abrmd) pkcs11Module;
112 | extraKeypairOptions = {
113 | token = "nixpkcs";
114 | };
115 | extraMachineOptions =
116 | { config, ... }:
117 | {
118 | virtualisation.tpm.enable = true;
119 | security.pkcs11.tpm2.enable = true;
120 | users.users.nginx = {
121 | extraGroups = [ "tss" ];
122 | };
123 | };
124 | };
125 | };
126 |
127 | packages = {
128 | inherit (pkgs)
129 | nebula
130 | openssl
131 | opensc
132 | pkcs11-provider
133 | nss_latest
134 | yubico-piv-tool
135 | tpm2-pkcs11
136 | yubihsm-shell
137 | ;
138 | };
139 | };
140 | };
141 | }
142 |
--------------------------------------------------------------------------------
/nixos/tests/nginx.nix:
--------------------------------------------------------------------------------
1 | {
2 | lib,
3 | testers,
4 | openssl,
5 | curl,
6 | nginx,
7 | pkgs,
8 | self,
9 | nixpkgs,
10 | pkcs11Module,
11 | extraKeypairOptions,
12 | extraMachineOptions ? { },
13 | }:
14 |
15 | let
16 | soPinFile = "/etc/so.pin";
17 | pinFile = "/etc/user.pin";
18 | extraEnv = pkcs11Module.mkEnv { };
19 |
20 | storeInitHook = pkgs.writeShellScript "store-init" ''
21 | chown -R nginx:nginx "$NIXPKCS_STORE_DIR" || true
22 | '';
23 |
24 | mkNode =
25 | {
26 | name,
27 | extraConfig ? { },
28 | }:
29 | lib.mkMerge [
30 | (
31 | { config, ... }:
32 | {
33 | imports = [
34 | self.nixosModules.default
35 | ];
36 |
37 | security.pkcs11 = {
38 | enable = true;
39 | keypairs = {
40 | ${name} = lib.recursiveUpdate {
41 | enable = true;
42 | inherit pkcs11Module extraEnv storeInitHook;
43 | id = "deadbeefcafe";
44 | debug = true;
45 | keyOptions = {
46 | algorithm = "RSA";
47 | type = "3072";
48 | usage = [
49 | "sign"
50 | "derive"
51 | ];
52 | inherit soPinFile;
53 | };
54 | certOptions = {
55 | serial = "09f91102";
56 | subject = "C=US/ST=California/L=Carlsbad/O=nixpkcs/CN=nixpkcs.local";
57 | extensions = [
58 | "v3_ca"
59 | "keyUsage=critical,nonRepudiation,keyCertSign,digitalSignature,cRLSign"
60 | "subjectAltName=DNS:nixpkcs.local"
61 | ];
62 | validityDays = 14;
63 | renewalPeriod = 7;
64 | inherit pinFile;
65 | writeTo = "/etc/keys/nixpkcs.local.crt";
66 | };
67 | } extraKeypairOptions;
68 | };
69 | };
70 |
71 | system.activationScripts.initTest.text = ''
72 | if [ ! -f ${lib.escapeShellArg pinFile} ]; then
73 | echo -n 22446688 > ${lib.escapeShellArg pinFile}
74 | chmod 0640 ${lib.escapeShellArg pinFile}
75 | chown nginx:nginx ${lib.escapeShellArg pinFile} || true
76 | fi
77 | if [ ! -f ${lib.escapeShellArg soPinFile} ]; then
78 | # If we are logging in as the user, place the user PIN in the SO PIN file.
79 | ${
80 | if config.security.pkcs11.keypairs.${name}.keyOptions.loginAsUser then
81 | ''
82 | ln -s ${lib.escapeShellArg pinFile} ${lib.escapeShellArg soPinFile}
83 | ''
84 | else
85 | ''
86 | echo -n 11335577 > ${lib.escapeShellArg soPinFile}
87 | chmod 0600 ${lib.escapeShellArg soPinFile}
88 | ''
89 | }
90 | fi
91 | mkdir -p /etc/keys
92 | '';
93 |
94 | services.nginx = {
95 | enable = true;
96 | package = openssl.withPkcs11Module {
97 | inherit pkcs11Module;
98 | package = nginx;
99 | confName = "nginx";
100 | passthru = {
101 | inherit (nginx) modules;
102 | };
103 | };
104 | recommendedOptimisation = true;
105 | recommendedTlsSettings = true;
106 | recommendedProxySettings = true;
107 | recommendedGzipSettings = true;
108 |
109 | appendConfig = ''
110 | ${lib.concatMapStringsSep "\n" (x: "env ${x};") (lib.attrNames extraEnv)}
111 | '';
112 |
113 | virtualHosts."nixpkcs.local" = {
114 | forceSSL = true;
115 | sslCertificate = "/etc/keys/nixpkcs.local.crt";
116 | sslCertificateKey = pkgs.pkcs11-provider.uri2pem config.security.pkcs11.keypairs.${name}.uri;
117 | locations."/" = {
118 | root = ./nginx/root;
119 | };
120 | };
121 | };
122 |
123 | networking.hosts = {
124 | "127.0.0.1" = [ "nixpkcs.local" ];
125 | };
126 |
127 | environment = {
128 | systemPackages = [
129 | openssl
130 | curl
131 | ];
132 | };
133 |
134 | systemd.services."nginx" = {
135 | environment = extraEnv;
136 | serviceConfig = {
137 | # For accessing the TPM2 database.
138 | ProtectSystem = lib.mkForce false;
139 |
140 | # For accessing the TPM2 device.
141 | PrivateDevices = lib.mkForce false;
142 | };
143 | };
144 | }
145 | )
146 | extraConfig
147 | extraMachineOptions
148 | ];
149 | in
150 | testers.runNixOSTest {
151 | name = "nixpkcs-test-nginx";
152 |
153 | nodes.nginx = mkNode {
154 | name = "nginx";
155 | };
156 |
157 | testScript = ''
158 | nginx.start()
159 |
160 | # Wait for the keys to exist.
161 | nginx.wait_until_succeeds('openssl x509 -in /etc/keys/nixpkcs.local.crt -noout -subject | grep -q "nixpkcs.local"')
162 |
163 | # It should be in nixpkcs-uri too.
164 | nginx.succeed("nixpkcs-uri | tee /dev/stderr | grep -qE '^nginx[[:space:]]+pkcs11:.*?;?id=%DE%AD%BE%EF%CA%FE(;|$)'")
165 |
166 | # Wait for the webserver to come up, and make sure it's reliable thereafter
167 | nginx.succeed('systemctl restart nginx')
168 | cmd = 'curl --cacert /etc/keys/nixpkcs.local.crt https://nixpkcs.local/index.html | tee /dev/stderr | grep "Hello, nixpkcs!"'
169 | nginx.wait_until_succeeds(cmd)
170 | for i in range(100):
171 | nginx.succeed(cmd)
172 | '';
173 | }
174 |
--------------------------------------------------------------------------------
/COPYING.md:
--------------------------------------------------------------------------------
1 | # GNU LESSER GENERAL PUBLIC LICENSE
2 |
3 | Version 3, 29 June 2007
4 |
5 | Copyright (C) 2007 Free Software Foundation, Inc.
6 |
7 |
8 | Everyone is permitted to copy and distribute verbatim copies of this
9 | license document, but changing it is not allowed.
10 |
11 | This version of the GNU Lesser General Public License incorporates the
12 | terms and conditions of version 3 of the GNU General Public License,
13 | supplemented by the additional permissions listed below.
14 |
15 | ## 0. Additional Definitions.
16 |
17 | As used herein, "this License" refers to version 3 of the GNU Lesser
18 | General Public License, and the "GNU GPL" refers to version 3 of the
19 | GNU General Public License.
20 |
21 | "The Library" refers to a covered work governed by this License, other
22 | than an Application or a Combined Work as defined below.
23 |
24 | An "Application" is any work that makes use of an interface provided
25 | by the Library, but which is not otherwise based on the Library.
26 | Defining a subclass of a class defined by the Library is deemed a mode
27 | of using an interface provided by the Library.
28 |
29 | A "Combined Work" is a work produced by combining or linking an
30 | Application with the Library. The particular version of the Library
31 | with which the Combined Work was made is also called the "Linked
32 | Version".
33 |
34 | The "Minimal Corresponding Source" for a Combined Work means the
35 | Corresponding Source for the Combined Work, excluding any source code
36 | for portions of the Combined Work that, considered in isolation, are
37 | based on the Application, and not on the Linked Version.
38 |
39 | The "Corresponding Application Code" for a Combined Work means the
40 | object code and/or source code for the Application, including any data
41 | and utility programs needed for reproducing the Combined Work from the
42 | Application, but excluding the System Libraries of the Combined Work.
43 |
44 | ## 1. Exception to Section 3 of the GNU GPL.
45 |
46 | You may convey a covered work under sections 3 and 4 of this License
47 | without being bound by section 3 of the GNU GPL.
48 |
49 | ## 2. Conveying Modified Versions.
50 |
51 | If you modify a copy of the Library, and, in your modifications, a
52 | facility refers to a function or data to be supplied by an Application
53 | that uses the facility (other than as an argument passed when the
54 | facility is invoked), then you may convey a copy of the modified
55 | version:
56 |
57 | - a) under this License, provided that you make a good faith effort
58 | to ensure that, in the event an Application does not supply the
59 | function or data, the facility still operates, and performs
60 | whatever part of its purpose remains meaningful, or
61 | - b) under the GNU GPL, with none of the additional permissions of
62 | this License applicable to that copy.
63 |
64 | ## 3. Object Code Incorporating Material from Library Header Files.
65 |
66 | The object code form of an Application may incorporate material from a
67 | header file that is part of the Library. You may convey such object
68 | code under terms of your choice, provided that, if the incorporated
69 | material is not limited to numerical parameters, data structure
70 | layouts and accessors, or small macros, inline functions and templates
71 | (ten or fewer lines in length), you do both of the following:
72 |
73 | - a) Give prominent notice with each copy of the object code that
74 | the Library is used in it and that the Library and its use are
75 | covered by this License.
76 | - b) Accompany the object code with a copy of the GNU GPL and this
77 | license document.
78 |
79 | ## 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that, taken
82 | together, effectively do not restrict modification of the portions of
83 | the Library contained in the Combined Work and reverse engineering for
84 | debugging such modifications, if you also do each of the following:
85 |
86 | - a) Give prominent notice with each copy of the Combined Work that
87 | the Library is used in it and that the Library and its use are
88 | covered by this License.
89 | - b) Accompany the Combined Work with a copy of the GNU GPL and this
90 | license document.
91 | - c) For a Combined Work that displays copyright notices during
92 | execution, include the copyright notice for the Library among
93 | these notices, as well as a reference directing the user to the
94 | copies of the GNU GPL and this license document.
95 | - d) Do one of the following:
96 | - 0) Convey the Minimal Corresponding Source under the terms of
97 | this License, and the Corresponding Application Code in a form
98 | suitable for, and under terms that permit, the user to
99 | recombine or relink the Application with a modified version of
100 | the Linked Version to produce a modified Combined Work, in the
101 | manner specified by section 6 of the GNU GPL for conveying
102 | Corresponding Source.
103 | - 1) Use a suitable shared library mechanism for linking with
104 | the Library. A suitable mechanism is one that (a) uses at run
105 | time a copy of the Library already present on the user's
106 | computer system, and (b) will operate properly with a modified
107 | version of the Library that is interface-compatible with the
108 | Linked Version.
109 | - e) Provide Installation Information, but only if you would
110 | otherwise be required to provide such information under section 6
111 | of the GNU GPL, and only to the extent that such information is
112 | necessary to install and execute a modified version of the
113 | Combined Work produced by recombining or relinking the Application
114 | with a modified version of the Linked Version. (If you use option
115 | 4d0, the Installation Information must accompany the Minimal
116 | Corresponding Source and Corresponding Application Code. If you
117 | use option 4d1, you must provide the Installation Information in
118 | the manner specified by section 6 of the GNU GPL for conveying
119 | Corresponding Source.)
120 |
121 | ## 5. Combined Libraries.
122 |
123 | You may place library facilities that are a work based on the Library
124 | side by side in a single library together with other library
125 | facilities that are not Applications and are not covered by this
126 | License, and convey such a combined library under terms of your
127 | choice, if you do both of the following:
128 |
129 | - a) Accompany the combined library with a copy of the same work
130 | based on the Library, uncombined with any other library
131 | facilities, conveyed under the terms of this License.
132 | - b) Give prominent notice with the combined library that part of it
133 | is a work based on the Library, and explaining where to find the
134 | accompanying uncombined form of the same work.
135 |
136 | ## 6. Revised Versions of the GNU Lesser General Public License.
137 |
138 | The Free Software Foundation may publish revised and/or new versions
139 | of the GNU Lesser General Public License from time to time. Such new
140 | versions will be similar in spirit to the present version, but may
141 | differ in detail to address new problems or concerns.
142 |
143 | Each version is given a distinguishing version number. If the Library
144 | as you received it specifies that a certain numbered version of the
145 | GNU Lesser General Public License "or any later version" applies to
146 | it, you have the option of following the terms and conditions either
147 | of that published version or of any later version published by the
148 | Free Software Foundation. If the Library as you received it does not
149 | specify a version number of the GNU Lesser General Public License, you
150 | may choose any version of the GNU Lesser General Public License ever
151 | published by the Free Software Foundation.
152 |
153 | If the Library as you received it specifies that a proxy can decide
154 | whether future versions of the GNU Lesser General Public License shall
155 | apply, that proxy's public statement of acceptance of any version is
156 | permanent authorization for you to choose that version for the
157 | Library.
158 |
--------------------------------------------------------------------------------
/nixos/tests/nebula.nix:
--------------------------------------------------------------------------------
1 | {
2 | lib,
3 | testers,
4 | nebula,
5 | openssl,
6 | pkgs,
7 | self,
8 | nixpkgs,
9 | pkcs11Module,
10 | baseKeyId ? 0,
11 | extraKeypairOptions ? { },
12 | extraMachineOptions ? { },
13 | }:
14 |
15 | let
16 | # We'll need to be able to trade cert files between nodes via scp.
17 | inherit (import "${nixpkgs}/nixos/tests/ssh-keys.nix" pkgs)
18 | snakeOilPrivateKey
19 | snakeOilPublicKey
20 | ;
21 |
22 | soPinFile = "/etc/so.pin";
23 | pinFile = "/etc/user.pin";
24 | extraEnv = pkcs11Module.mkEnv { };
25 |
26 | mkNode =
27 | {
28 | name,
29 | realIp,
30 | staticHostMap ? null,
31 | extraConfig ? { },
32 | }:
33 | lib.mkMerge [
34 | (
35 | { config, ... }:
36 | {
37 | imports = [
38 | self.nixosModules.default
39 | ];
40 | networking = {
41 | hostName = name;
42 | interfaces.eth1 = {
43 | ipv4.addresses = lib.mkForce [
44 | {
45 | address = realIp;
46 | prefixLength = 24;
47 | }
48 | ];
49 | useDHCP = false;
50 | };
51 | };
52 |
53 | security.pkcs11 = {
54 | enable = true;
55 | keypairs = {
56 | ${name} = lib.recursiveUpdate {
57 | enable = true;
58 | inherit pkcs11Module extraEnv;
59 | id = baseKeyId;
60 | debug = true;
61 | keyOptions = {
62 | algorithm = "EC";
63 | type = "secp256r1";
64 | usage = [
65 | "sign"
66 | "derive"
67 | "decrypt"
68 | "wrap"
69 | ];
70 | inherit soPinFile;
71 | };
72 | certOptions = {
73 | serial = "09f91102";
74 | subject = "C=US/ST=California/L=Carlsbad/O=nixpkcs/CN=NixOS User ${name}'s Certificate";
75 | extensions = [
76 | # optional, but a good thing to test
77 | "v3_ca"
78 | "keyUsage=critical,nonRepudiation,keyCertSign,digitalSignature,cRLSign"
79 | ];
80 | validityDays = 14;
81 | renewalPeriod = 7;
82 | inherit pinFile;
83 | writeTo = "/home/${name}/${name}.crt";
84 | };
85 | } extraKeypairOptions;
86 | };
87 | };
88 |
89 | system.activationScripts.initTest.text = ''
90 | ${lib.optionalString (config.networking.hostName != "mallory") ''
91 | # No SSH private key for Mallory.
92 | if [ ! -d /root/.ssh ]; then
93 | mkdir -p /root/.ssh
94 | chown 700 /root/.ssh
95 | cat ${lib.escapeShellArg snakeOilPrivateKey} > /root/.ssh/id_snakeoil
96 | chown 600 /root/.ssh/id_snakeoil
97 | fi
98 | ''}
99 | if [ ! -f ${lib.escapeShellArg pinFile} ]; then
100 | echo -n 22446688 > ${lib.escapeShellArg pinFile}
101 | chmod 0640 ${lib.escapeShellArg pinFile}
102 | chown root:nebula-nixpkcs ${lib.escapeShellArg pinFile} || true
103 | fi
104 | if [ ! -f ${lib.escapeShellArg soPinFile} ]; then
105 | # If we are logging in as the user, place the user PIN in the SO PIN file.
106 | ${
107 | if config.security.pkcs11.keypairs.${name}.keyOptions.loginAsUser then
108 | ''
109 | ln -s ${lib.escapeShellArg pinFile} ${lib.escapeShellArg soPinFile}
110 | ''
111 | else
112 | ''
113 | echo -n 11335577 > ${lib.escapeShellArg soPinFile}
114 | chmod 0600 ${lib.escapeShellArg soPinFile}
115 | ''
116 | }
117 | fi
118 | ${lib.optionalString ((config.security.pkcs11.keypairs.${name}.uri or null) != null) ''
119 | mkdir -p /etc/nebula
120 | if [ ! -f /etc/nebula/${name}.key ]; then
121 | ${config.security.pkcs11.uri.package}/bin/nixpkcs-uri ${name} | tee /etc/nebula/${name}.key
122 | chown -R nebula-nixpkcs:nebula-nixpkcs /etc/nebula || true
123 | fi
124 | ''}
125 | ${lib.optionalString ((config.security.pkcs11.keypairs.ca.uri or null) != null) ''
126 | mkdir -p /etc/nebula/ca
127 | if [ ! -f /etc/nebula/ca/ca.key ]; then
128 | ${config.security.pkcs11.uri.package}/bin/nixpkcs-uri ca | tee /etc/nebula/ca/ca.key
129 | chown -R nebula-nixpkcs:nebula-nixpkcs /etc/nebula || true
130 | fi
131 | ''}
132 | '';
133 |
134 | services.openssh.enable = true;
135 |
136 | users.users = {
137 | ${name}.isNormalUser = true;
138 | root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
139 | };
140 |
141 | environment = {
142 | systemPackages = [
143 | nebula
144 | openssl
145 | ];
146 | };
147 | }
148 | )
149 | (lib.mkIf (staticHostMap != null) (
150 | { config, ... }:
151 | {
152 | services.nebula.networks.nixpkcs = {
153 | # Note that these paths won't exist when the machine is first booted.
154 | enable = lib.mkDefault true;
155 | ca = "/etc/nebula/ca.crt";
156 | cert = "/etc/nebula/${name}.crt";
157 | key = config.security.pkcs11.keypairs.${name}.uri;
158 | listen = {
159 | host = "0.0.0.0";
160 | port = 4242;
161 | };
162 | isLighthouse = true;
163 | lighthouses = builtins.attrNames staticHostMap;
164 | inherit staticHostMap;
165 | firewall = {
166 | outbound = [
167 | {
168 | port = "any";
169 | proto = "any";
170 | host = "any";
171 | }
172 | ];
173 | inbound = [
174 | {
175 | port = "any";
176 | proto = "any";
177 | host = "any";
178 | }
179 | ];
180 | };
181 | };
182 |
183 | # So we pass down PKCS#11 environment variables to Nebula.
184 | systemd.services."nebula@nixpkcs" = {
185 | environment = extraEnv;
186 | };
187 | }
188 | ))
189 | extraConfig
190 | extraMachineOptions
191 | ];
192 | in
193 | testers.runNixOSTest {
194 | name = "nixpkcs-test-nebula";
195 |
196 | nodes = {
197 | # First participant.
198 | alice = mkNode {
199 | name = "alice";
200 | realIp = "192.168.1.1";
201 | staticHostMap = {
202 | "10.32.0.2" = [ "192.168.1.2:4242" ]; # Bob
203 | "10.32.0.3" = [ "192.168.1.200:4242" ]; # Mallory
204 | };
205 | };
206 |
207 | # Second participant.
208 | bob = mkNode {
209 | name = "bob";
210 | realIp = "192.168.1.2";
211 | staticHostMap = {
212 | "10.32.0.1" = [ "192.168.1.1:4242" ]; # Alice
213 | "10.32.0.3" = [ "192.168.1.200:4242" ]; # Mallory
214 | };
215 | };
216 |
217 | # The CA.
218 | charlie = mkNode {
219 | name = "charlie";
220 | realIp = "192.168.1.100";
221 | };
222 |
223 | # Hintjens, 2015
224 | mallory = mkNode {
225 | name = "mallory";
226 | realIp = "192.168.1.200";
227 | staticHostMap = {
228 | "10.32.0.1" = [ "192.168.1.1:4242" ]; # Alice
229 | "10.32.0.2" = [ "192.168.1.2:4242" ]; # Bob
230 | };
231 | extraConfig = {
232 | security.pkcs11 = {
233 | enable = true;
234 | keypairs = {
235 | ca = lib.recursiveUpdate {
236 | enable = true;
237 | inherit pkcs11Module extraEnv;
238 | id = baseKeyId + 1;
239 | debug = true;
240 | keyOptions = {
241 | algorithm = "EC";
242 | type = "secp256r1";
243 | usage = [
244 | "sign"
245 | "derive"
246 | "decrypt"
247 | "wrap"
248 | ];
249 | inherit soPinFile;
250 | };
251 | certOptions = {
252 | serial = "66666666";
253 | subject = "C=US/ST=California/L=Carlsbad/O=nixpkcs/CN=Mallory's Super Legit CA";
254 | validityDays = 3650;
255 | inherit pinFile;
256 | writeTo = "/home/mallory/ca.crt";
257 | };
258 | } extraKeypairOptions;
259 | };
260 | };
261 | };
262 | };
263 | };
264 |
265 | testScript =
266 | let
267 | sshOpts = "-oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null -oIdentityFile=/root/.ssh/id_snakeoil";
268 | in
269 | ''
270 | # Boot them all up.
271 | for machine in (alice, bob, charlie):
272 | machine.start()
273 |
274 | # Wait for the keys to exist.
275 | for machine in (alice, bob, charlie):
276 | machine.wait_until_succeeds('openssl x509 -in /home/{0}/{0}.crt -noout -subject | grep -q "NixOS User {0}\'s Certificate"'.format(machine.name))
277 | machine.succeed("nixpkcs-uri | tee /dev/stderr | grep -qE '^{0}[[:space:]]+pkcs11:.*?;?id=[%0-9A-F]+(;|$)'".format(machine.name))
278 |
279 | # Charlie is the root of trust.
280 | charlie.succeed('nebula-cert ca -curve P256 -name charlie -ips 10.32.0.0/16 -pkcs11 "$(.openssl` and `.opensc` passthrus
76 | - Support pkcs11-provider's debug environment variables with the OpenSSL wrapper
77 | - 1.1.0 ("Beta BER"): Many new features.
78 | - **Fully declarative TPM2 and NSS store initialization!** You now don't need to do anything imperative to initialize a TPM2 or NSS store using nixpkcs.
79 | - **Nginx and Nebula support**, featuring integration tests with TPM2 and NSS
80 | - Note that first requests to nginx may cause a dbus timeout until the key is loaded, but subsequent requests are fast
81 | - New store initialization hook
82 | - Updated rekeying hook to take the key name in $1
83 | - 1.0 ("Alpha ASN"): Initial release
84 |
85 | ## Supported PKCS#11 consumers
86 |
87 | These consumers are supported via a wrapper accessible via `withPkcs11Module`.
88 |
89 | |Package|Passthru|Description|
90 | |:------|:-------|:----------|
91 | |`openssl`|withPkcs11Module|Wraps any OpenSSL-linked application with a special OPENSSL_CONF enabling the pkcs11-provider OpenSSL provider or the p11-kit OpenSSL engine. Outputs a derivation created with symlinkJoin, `${name}-with-pkcs11`.|
92 | |`opensc`|withPkcs11Module|Wraps `pkcs11-tool` with the given `pkcs11Module`.|
93 |
94 | ## Patched packages
95 |
96 | These packages have PKCS#11 support added via a patch.
97 |
98 | - `nebula`: Added PKCS#11 support to version 1.9.3
99 | - `tpm2-pkcs11`: Support shared secret (Diffie-Hellman) derivation, and disable FAPI warnings
100 |
101 | ## Added packages
102 |
103 | These packages were added:
104 |
105 | - `nixpkcs-uri`: Prints a PKCS#11 URI for a given key.
106 | - Pass a key name as $1, and it prints the PKCS#11 URI on stdout.
107 | - Pass no keys to list all of them, or multiple to list multiple of them.
108 | - `pkcs11-provider.uri2pem`: Converts a PKCS#11 URI to PEM.
109 | - Supports functor syntax: `pkcs11-provider.uri2pem "pkcs11:..."` produces a PEM file in the Nix store
110 | that corresponds to a PKCS#11 URI.
111 |
112 | ## Supported PKCS#11 providers
113 |
114 | As of version 1.1.1, you can use the passthru syntax to automatically get an PKCS#11 consumer that uses a particular PKCS#11 module (for instance, `yubico-piv-tool.openssl` or `tpm2-pkcs11.opensc`).
115 |
116 | - `yubico-piv-tool.pkcs11Module` (Yubikey)
117 | - `tpm2-pkcs11.pkcs11Module` (TPM2)
118 | - `opensc.pkcs11Module` (NitroKey)
119 | - `yubihsm-shell.pkcs11Module` (YubiHSM)
120 | - `nss_latest.pkcs11Module` (NSS, for testing)
121 |
122 | ## Quickstart: Key Management
123 |
124 | - Add this flake's NixOS module to your imports: `imports = [ nixpkcs.nixosModules.default ]`
125 | - Load this flake as an overlay with something like: `nixpkgs.overlays = [ nixpkcs.overlays.default ]`
126 | - Choose your PKCS#11 module provider from the list above.
127 | - Write keypair definitions
128 | - Keys will automatically be generated!
129 |
130 | ### Example
131 |
132 | Since a Nix config speaks a thousand words, here are examples for both Yubikey and TPM.
133 | The Yubikey-specific config parts are commented below.
134 |
135 | ```nix
136 | security.pkcs11 = {
137 | enable = true;
138 | pcsc = {
139 | enable = true;
140 | };
141 | keypairs = {
142 | my-key = {
143 | enable = true;
144 |
145 | # The PKCS#11 module to use.
146 | inherit (tpm2-pkcs11) pkcs11Module;
147 | # inherit (yubico-piv-tool) pkcs11Module;
148 |
149 | # Script that runs after initializing the store for the first time,
150 | # for tokens that require a state directory (TPM2, for example).
151 | # storeInitHook = pkgs.writeShellScript "store-init-hook" '''
152 | # chown -R alice:users "$NIXPKCS_STORE_DIR"
153 | # ''
154 |
155 | # The token name. For TPM, this can be whatever you want, as long as it's consistent.
156 | # The default is `nixpkcs`; `pkcs11-tool --list-slots` will tell you for other tokens.
157 | # token = "nixpkcs";
158 | # token = "YubiKey PIV #123456";
159 |
160 | # The key ID.
161 | # For yubikey, note the key mapping:
162 | # https://developers.yubico.com/yubico-piv-tool/YKCS11/
163 | id = 1;
164 |
165 | # Not required for all tokens, but is for NSS.
166 | # slot = 2;
167 |
168 | # Automatically generated; generally you don't need to change the default.
169 | # If you need to access this, you can use `config.nixpkcs.my-key.uri` in your config.
170 | # uri = "pkcs11:token=...";
171 |
172 | # In case you want the fully RFC compliant version with no extra parameters.
173 | # p11kit requires this, but you shouldn't unless you really need it.
174 | # rfc7512Uri = "pkcs11:token=..."
175 |
176 | # Environment variables we should pass to the script.
177 | # Defaults to `pkcs11Module.mkEnv {}`. If overridden, make sure to include those.
178 | # extraEnv = { MY_ENV_VARIABLE = 42; };
179 |
180 | # Enables very verbose debug output.
181 | # debug = true;
182 |
183 | # Options for the private key.
184 | keyOptions = {
185 | # EC or RSA.
186 | algorithm = "EC";
187 |
188 | # The bits (for RSA) or the curve (for ECDSA).
189 | type = "secp256r1";
190 |
191 | # Options: sign, derive, decrypt, wrap
192 | usage = ["sign" "derive"];
193 |
194 | # Security Officer PIN. For the yubikey, this is the management token.
195 | # At least 8 digits, maybe more. For the Yubikey, it's a 40 char hex string.
196 | soPinFile = "/etc/mgmt.pin";
197 |
198 | # Causes a missing token (e.g. a disconnected yubikey) to not fail the systemd unit.
199 | # softFail = false;
200 |
201 | # Warning! This will regenerate the key every day and at boot. Useful for testing.
202 | # force = true;
203 |
204 | # Destroys the old key and certificate on the token upon renewal. Default to false.
205 | # This may be useful to set to true if you truly do not care about the old key slots.
206 | # destroyOld = false;
207 |
208 | # Needed for the Yubikey, but not needed for TPM and NSS.
209 | # loginAsUser = false;
210 | };
211 |
212 | # Options for the cert.
213 | certOptions = {
214 | # The certificate message digest. `openssl list -digest-commands` for the list. Case insensitive.
215 | # digest = "SHA256";
216 |
217 | # Can be omitted for a random certificate serial.
218 | # serial = "09f91102";
219 |
220 | # Certificate (and key) validity in days.
221 | validityDays = 365 * 3;
222 |
223 | # When this key should be generated. nixpkcs won't do anything until this date,
224 | # but you still can reference the key-to-be in your configs.
225 | # validStarting = "2026-01-01";
226 |
227 | # Number of days prior to expiration this key should be renewed and replaced.
228 | # Set to less than 0 to disable auto-renewal.
229 | # renewalPeriod = -1;
230 |
231 | # The subject.
232 | subject = "C=US/ST=California/L=Carlsbad/O=nixpkcs/CN=My CA Cert";
233 |
234 | # Extensions to add.
235 | # Certificate authority:
236 | extensions = [
237 | "v3_ca"
238 | "keyUsage=critical,nonRepudiation,keyCertSign,digitalSignature,cRLSign"
239 | ];
240 |
241 | # Server certificate:
242 | # extensions = [
243 | # "basicConstraints=critical,CA:FALSE"
244 | # "keyUsage=critical,digitalSignature,keyEncipherment,keyAgreement"
245 | # "extendedKeyUsage=serverAuth"
246 | # "subjectAltName=DNS:example.com"
247 | # ];
248 |
249 | # Client certificate:
250 | # extensions = [
251 | # "basicConstraints=critical,CA:FALSE"
252 | # "keyUsage=critical,digitalSignature,keyEncipherment"
253 | # "extendedKeyUsage=clientAuth"
254 | # ];
255 |
256 | # File containing the user PIN. Usually 8 digits but can be more.
257 | pinFile = "/etc/user.pin";
258 |
259 | # If provided, will write the certificate here.
260 | writeTo = "/home/alice/ca.crt";
261 |
262 | # Called whenever nixpkcs runs. Can be used to restart services. See the module documentation for examples.
263 | # rekeyHook = pkgs.writeShellScript "rekey-hook" ''
264 | # if [ "$2" == 'new' ]; then
265 | # cat > "/home/alice/$1.crt"
266 | # chown alice:alice "/home/alice/$1.crt"
267 | # fi
268 | # ''
269 | };
270 | };
271 | };
272 | };
273 | ```
274 |
275 | ### NixOS module
276 |
277 | To automatically manage keys, you will need to use the NixOS module.
278 |
279 | |Option|Default|Description|Example|
280 | |:-----|:------|:----------|:------|
281 | |`security.pkcs11.enable`|false|Enables automated key management|`security.pkcs11.enable = true`|
282 | |`security.pkcs11.pcsc.enable`|false|Enables the PCSC smartcard daemon. You will need this for Yubikeys.|`security.pkcs11.pcsc.enable = true`|
283 | |`security.pkcs11.pcsc.users`|[]|Sets the users that can access smartcards other than root.|`security.pkcs11.pcsc.users = ["alice" "bob"]`|
284 | |`security.pkcs11.tpm2.enable`|false|Enables TPM2 and tpm2-abrmd (the [TPM Access Broker and Resource Daemon](https://github.com/tpm2-software/tpm2-abrmd)). You will obviously need this for TPM2.|`true`|
285 | |`security.pkcs11.environment.enable`|true|Adds all keypair extraEnv to the system environment. Useful if a program you would like to use isn't wrapped with nixpkcs.|true|
286 | |`security.pkcs11.uri.enable`|true|Enables the `nixpkcs-uri` command, converting keypair names to PKCS#11 URIs.|true|
287 | |`security.pkcs11.uri.package`|nixpkcs-uri|The package to use for the `nixpkcs-uri` command.||
288 | |`security.pkcs11.keypairs.`|N/A|Each keypair.|See above|
289 |
290 | ## Quickstart: Consuming a PKCS#11 module
291 |
292 | Some packages need to be wrapped to support PKCS#11 keys. The `withPkcs11Module` interface lets you do this.
293 |
294 | ### OpenSSL wrapper: `openssl.withPkcs11Module`
295 |
296 | - `pkcs11Module`: The PKCS#11 module. Usually `my-package.pkcs11Module`.
297 | - `package`: The package to wrap. Defaults to `openssl.bin`.
298 | - `confName`: The config name. Defaults to `"openssl_conf"`. (For instance, nodejs requires `nodejs_conf`).
299 | - `engineName`: The name of the OpenSSL engine, if engines are enabled. (Default: `pkcs11`)
300 | - `enableLegacyEngine`: True if we should enable `p11-kit` as an OpenSSL engine. (Default: false, since we prefer OpenSSL providers, which [are not deprecated, unlike engines](https://github.com/openssl/openssl/blob/master/README-ENGINES.md#deprecation-note)). **NOTE**: Enabling this _and_ the provider may cause strange things to happen.
301 | - `extraEngineOptions`: Extra options to pass to the engine config.
302 | - `providerName`: The name of the OpenSSL provider (Default: `pkcs11`)
303 | - `enableProvider`: True if we should enable the OpenSSL provider. (Default: true)
304 | - `extraProviderOptions`: Extra options to pass to the provider config. See [the docs](https://github.com/latchset/pkcs11-provider/blob/main/docs/provider-pkcs11.7.md#configuration).
305 | - `debug`: Enables verbose logging.
306 |
307 | ### Example: Wrapping node.js
308 |
309 | The following will produce a node.js with OPENSSL_CONF pointing to a config that uses `pkcs11-provider`:
310 |
311 | ```nix
312 | openssl.withPkcs11Module {
313 | inherit (yubico-piv-tool) pkcs11Module;
314 | package = nodejs;
315 | confName = "nodejs_conf";
316 | }
317 | ```
318 |
319 | This wrapped nodejs can be invoked just like normal nodejs, except that reading PEM files containing references to PKCS#11 keys works now:
320 |
321 | ```bash
322 | ./result/bin/node -e "const crypto = require('node:crypto'); const fs = require('node:fs'); const privkey = crypto.createPrivateKey(fs.readFileSync('provider.pem').toString('ascii')); const sign = crypto.createSign('SHA256'); sign.update('hello, node'); sign.end(); console.log(sign.sign(privkey));"
323 |
324 | ```
325 |
326 | ## Configuring a Yubikey
327 |
328 | To do many interesting things with private keys, you might need a certificate authority.
329 |
330 | Generally, a single level CA will be sufficient, though you can set up a multi tiered CA if you have multiple Yubikeys or certificate slots you'd like to use.
331 |
332 | Leaf certificates will be for clients and servers. A single root certificate (or a root and intermediate) will be used for signing other certificates.
333 |
334 | 1. Configure your Yubikey using [ykman](https://search.nixos.org/packages?channel=unstable&from=0&size=50&sort=relevance&type=packages&query=yubikey-manager).
335 | 1. Make sure the CCID interface is enabled.
336 | - Yubikey 4: `ykman config mode OTP+FIDO+CCID`.
337 | - Yubikey 5: `ykman config usb`
338 | 2. Set `security.pkcs11.enable = true`.
339 | 3. Optionally set `security.pkcs11.pcsc.users = ["your username"]` so the correct users can access the Yubikey as a smartcard.
340 |
341 | 2. Set up your [PIN, PUK (PIN Unlock Key), and Management Key](https://developers.yubico.com/PIV/Introduction/Admin_access.html) with **either** ykman or [yubico-piv-tool](https://search.nixos.org/packages?channel=unstable&from=0&size=50&sort=relevance&type=packages&query=yubico-piv-tool).
342 | - Both are in nixpkgs. ykman may be slightly easier, but yubico-piv-tool provides a few more options. Note the [defaults](https://developers.yubico.com/PIV/Introduction/Admin_access.html) for the PIN, PUK, and Management Key. Keep them in a safe place.
343 | - Management Key: `yubico-piv-tool -a set-mgm-key`
344 | - PIN: `yubico-piv-tool -a change-pin`
345 | - PUK: `yubico-piv-tool -a change-puk`
346 | - Take note of the [PIV certificate slots](https://developers.yubico.com/PIV/Introduction/Certificate_slots.html).
347 | - Use slot 9c for root certificate keys, as a PIN is always required. Use slots 9d/82-95 for any keys where a PIN is optional, like TLS web server or client keys.
348 | - Decide what kind of keys you want to generate.
349 | - I generally go for ECC P-384 or RSA 3072. Since my Yubikey's PIV application doesn't support RSA 3072, I stuck with P-384.
350 |
351 | 3. Add the keys you want to generate to your NixOS configuration.
352 |
353 | 4. Use the keys!
354 |
355 | ## Contributing
356 |
357 | All contributions to this project are licensed under the terms of the GNU Lesser General Public License, version 3.
358 |
359 | You are free to use this in commercial works; please open a PR if you make an improvement.
360 |
--------------------------------------------------------------------------------
/nixpkcs.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -euo pipefail
4 | shopt -s extglob
5 |
6 | script_path="${BASH_SOURCE[0]:-$0}"
7 | script_name="$(basename -- "$script_path")"
8 | label='unknown key'
9 |
10 | debug=0
11 | if [[ -v NIXPKCS_DEBUG ]] && [ -n "$NIXPKCS_DEBUG" ] && [ "$NIXPKCS_DEBUG" != '0' ]; then
12 | debug=1
13 | fi
14 |
15 | # Writes a log message.
16 | log() {
17 | local level="$1"
18 | shift
19 | echo "[$script_name/$level] ($label) $*" >&2
20 | }
21 |
22 | # Writes a debug message.
23 | debug() {
24 | if [ $debug -ne 0 ]; then
25 | log 'D' "$@"
26 | fi
27 | }
28 |
29 | # Writes an info message.
30 | info() {
31 | log 'I' "$@"
32 | }
33 |
34 | # Writes a warning message.
35 | warn() {
36 | log 'W' "$@"
37 | }
38 |
39 | # Writes an error message.
40 | error() {
41 | log 'E' "$@"
42 | }
43 |
44 | # Cleans up the temp directory.
45 | _TEMPDIR=''
46 | _TEMPDIR_PREFIX="nixpkcs."
47 | _TEMPNAME_FORMAT="${_TEMPDIR_PREFIX}XXXXXXXX"
48 | # shellcheck disable=SC2317,SC2329
49 | _tempdir_cleanup() {
50 | if [ -n "$_TEMPDIR" ] && [ -d "$_TEMPDIR" ] && [[ "$(basename -- "$_TEMPDIR")" == "$_TEMPDIR_PREFIX"* ]]; then
51 | rm -rf "$_TEMPDIR"
52 | _TEMPDIR=''
53 | fi
54 | }
55 |
56 | # Creates an empty file with extension $1 (default none) in the temp directory.
57 | # Sets _TEMP to the path of the file, which will be 0 bytes and only writeable
58 | # by the current user.
59 | _TEMP=''
60 | make_tempfile() {
61 | # Create the tempdir
62 | if [ -z "$_TEMPDIR" ] || [ ! -d "$_TEMPDIR" ]; then
63 | _TEMPDIR="$(mktemp -dt "$_TEMPNAME_FORMAT")"
64 | chmod 0700 "$_TEMPDIR"
65 | trap _tempdir_cleanup EXIT
66 | fi
67 |
68 | local filename="$_TEMPNAME_FORMAT"
69 | if [ $# -ge 1 ]; then
70 | # Add an extension.
71 | filename="$filename.$1"
72 | fi
73 |
74 | # Make sure the filename exists, only we can write to it, and it's empty, in that order
75 | filename="$(mktemp -p "$_TEMPDIR" -t "$filename")"
76 | touch "$filename"
77 | chmod 0600 "$filename"
78 | truncate -s0 "$filename"
79 | _TEMP="$filename"
80 | }
81 |
82 | # Verify that the key name was passed.
83 | if [ $# -lt 1 ]; then
84 | error "Usage: $0 "
85 | exit 1
86 | else
87 | label="$1"
88 | fi
89 |
90 | if [[ ! -v NIXPKCS_KEY_SPEC ]] || [ -z "$NIXPKCS_KEY_SPEC" ]; then
91 | error "NIXPKCS_KEY_SPEC was not set"
92 | exit 1
93 | fi
94 |
95 | # Acquire a lock if we need to.
96 | if [[ -v NIXPKCS_LOCK_FILE ]] && [ -n "$NIXPKCS_LOCK_FILE" ] && command -v flock &>/dev/null; then
97 | lockfile="$NIXPKCS_LOCK_FILE"
98 | unset NIXPKCS_LOCK_FILE
99 | debug "Acquiring lock on $lockfile"
100 | exec flock -F "$lockfile" "$script_path" "$@"
101 | exit 255
102 | fi
103 |
104 | use_label=1
105 | if [[ -v NIXPKCS_NO_LABELS ]] && [ -n "$NIXPKCS_NO_LABELS" ] && [ "$NIXPKCS_NO_LABELS" != '0' ]; then
106 | debug "Skipping labels because we were asked to."
107 | use_label=0
108 | fi
109 |
110 | # Deletes every non-alphanumeric, _, or -.
111 | _SECRET=''
112 | _SECRET_CHARS='0-9A-Za-z_-'
113 | sanitize_secret() {
114 | local secret="$1"
115 | _SECRET="${secret//[^$_SECRET_CHARS]/}"
116 | }
117 |
118 | # Validates that the secret in $1 contains only alphanumerics, _, or -.
119 | # Sets '_SECRET' if it's valid, or sets it to '' if not.
120 | validate_secret() {
121 | local secret="$1"
122 | if [[ "$secret" =~ ^[$_SECRET_CHARS]+$ ]]; then
123 | sanitize_secret "$secret"
124 | else
125 | _SECRET=''
126 | fi
127 | }
128 |
129 | # Reads the file at $1 containing a secret.
130 | # Sets '_SECRET' if it's valid and contains a secret, or '' and returns 1 if not.
131 | read_secret_file() {
132 | local secret_file="$1"
133 | if [ -n "$secret_file" ]; then
134 | if [ -f "$secret_file" ]; then
135 | validate_secret "$(<"$secret_file")"
136 | if [ -n "$_SECRET" ]; then
137 | return 0
138 | else
139 | _SECRET=''
140 | error "Secret file '$secret_file' didn't appear to contain a valid secret"
141 | return 1
142 | fi
143 | else
144 | _SECRET=''
145 | error "Secret file '$secret_file' was specified and didn't exist"
146 | return 1
147 | fi
148 | else
149 | _SECRET=''
150 | return 0
151 | fi
152 | }
153 |
154 | # Executes a command, logging it and redacting secrets.
155 | # Usage: log_exec (secrets) -- (command)
156 | log_exec() (
157 | local secrets=()
158 | local args=()
159 | local log_args=()
160 | local reading_args=0
161 |
162 | for arg in "$@"; do
163 | if [ $reading_args -eq 0 ]; then
164 | case "$arg" in
165 | --)
166 | reading_args=1
167 | ;;
168 | *)
169 | sanitize_secret "$arg"
170 | arg="$_SECRET"
171 | if [ -n "$arg" ]; then
172 | secrets+=("$arg")
173 | fi
174 | ;;
175 | esac
176 | else
177 | args+=("$arg")
178 |
179 | # Strip secrets from the logs.
180 | for secret in ${secrets[@]+"${secrets[@]}"}; do
181 | arg="${arg//$secret/\/\/REDACTED\/\/}"
182 | done
183 | log_args+=("$arg")
184 | fi
185 | done
186 |
187 | if [ "${#args[@]}" -lt 1 ]; then
188 | error "No arguments provided!"
189 | return 1
190 | fi
191 |
192 | # Clean up argv[0] for logging.
193 | log_args[0]="$(basename -- "${log_args[0]}")"
194 |
195 | info " $(printf '%q ' "${log_args[@]+"${log_args[@]}"}")"
196 | exec ${args[@]+"${args[@]}"}
197 | )
198 |
199 | # Runs pkcs11-tool.
200 | # $1: The operation mode (anonymous|user|so).
201 | # $@: The remaining pkcs11-tool args.
202 | p11tool() {
203 | local op_mode="$1"
204 | shift
205 |
206 | # Remove leading zeroes from the ID.
207 | local id_str
208 | id_str="$(printf '%016x' "$id")"
209 | if [[ "$id_str" =~ ^(00)+([0-9a-f]{2,})$ ]]; then
210 | id_str="${BASH_REMATCH[2]}"
211 | fi
212 |
213 | local args=(--token-label "$token" --id "$id_str")
214 |
215 | if [ $use_label -ne 0 ]; then
216 | args+=(--label "$label")
217 | fi
218 |
219 | local secrets=()
220 | case "$op_mode" in
221 | anonymous)
222 | ;;
223 | user)
224 | if [ -n "$cert_options_user_pin" ]; then
225 | args+=(--login --login-type user --pin "$cert_options_user_pin")
226 | secrets+=("$cert_options_user_pin")
227 | fi
228 | ;;
229 | so)
230 | if [ -n "$key_options_so_pin" ]; then
231 | if [ "$key_options_login_as_user" == 'true' ]; then
232 | # Don't use the Security Officer login even though we ordinarily would.
233 | args+=(--login --login-type user --pin "$key_options_so_pin")
234 | else
235 | args+=(--login --login-type so --so-pin "$key_options_so_pin")
236 | fi
237 | secrets+=("$key_options_so_pin")
238 | fi
239 | ;;
240 | *)
241 | error "Invalid operation mode: $op_mode"
242 | return 1
243 | ;;
244 | esac
245 |
246 | log_exec ${secrets[@]+"${secrets[@]}"} -- pkcs11-tool ${args[@]+"${args[@]}"} "$@"
247 | }
248 |
249 | # Runs OpenSSL, stripping secrets out of the log.
250 | ossl() {
251 | log_exec "$cert_options_user_pin" "$key_options_so_pin" -- openssl "$@"
252 | }
253 |
254 | # Reads a certificate from the yubikey.
255 | _CERT=''
256 | read_cert() {
257 | info "Reading certificate"
258 | local cert
259 | local result
260 | set +e
261 | cert="$(p11tool user --read-object --type cert | openssl x509 -inform der)"
262 | result=$?
263 | set -e
264 |
265 | _CERT=''
266 | if [ $result -eq 0 ]; then
267 | info "Read certificate successfully."
268 | _CERT="$cert"
269 | else
270 | error "Certificate read failed: $result"
271 | fi
272 | }
273 |
274 | # Probes the token presence. If this returns 0, the token is likely present.
275 | probe_token() {
276 | info "Probing token presence"
277 | local result
278 | set +e
279 | p11tool anonymous --list-slots
280 | result=$?
281 | set -e
282 | return $result
283 | }
284 |
285 | # Generates a key.
286 | gen_key() {
287 | if [ "$key_options_destroy_old" == 'true' ]; then
288 | info "Destroying old key"
289 | p11tool so --delete-object --type privkey || true
290 | p11tool so --delete-object --type cert || true
291 | fi
292 |
293 | info "Generating key"
294 | local usages=()
295 | for usage in sign derive decrypt wrap; do
296 | if [[ -v key_options_usage["$usage"] ]]; then
297 | usages+=("--usage-$usage")
298 | fi
299 | done
300 | p11tool so --keypairgen --key-type "${key_options_algorithm}:${key_options_type}" ${usages[@]+"${usages[@]}"}
301 |
302 | info "Self signing certificate"
303 | local args=()
304 | if [ -n "$cert_options_serial" ]; then
305 | args+=(-set_serial "0x$cert_options_serial")
306 | fi
307 |
308 | for ext in ${cert_options_extensions[@]+"${cert_options_extensions[@]}"}; do
309 | if [[ "$ext" = *=* ]]; then
310 | # These are key/value pairs.
311 | args+=(-addext "$ext")
312 | else
313 | # These are not (e.g. v3_ca).
314 | args+=(-extensions "$ext")
315 | fi
316 | done
317 |
318 | local certificate
319 | certificate="$(ossl req -provider pkcs11 -key "$uri" -new -x509 "-$cert_options_digest" \
320 | -subj "/$cert_options_subject" \
321 | -days "$cert_options_validity_days" ${args[@]+"${args[@]}"} -outform PEM)"
322 |
323 | info "Importing certificate"
324 |
325 | # As of OpenSC 0.26.0, we can't use /dev/stdin here and need to write the cert to a tempfile.
326 | make_tempfile der
327 | echo "$certificate" | openssl x509 -inform pem -outform der >> "$_TEMP"
328 | p11tool so --write-object "$_TEMP" --type cert
329 | }
330 |
331 | # Returns 0 if the cert is expiring or expired.
332 | # @param $1 the cert
333 | # @param $2 the renewal period in days
334 | is_expiring() {
335 | local cert="$1"
336 | local renewal_period="$2"
337 | local now not_after cutoff
338 |
339 | if [ "$renewal_period" -lt 0 ]; then
340 | warn "Certificate renewal checking is disabled."
341 | return 1
342 | fi
343 |
344 | renewal_period=$((renewal_period * 86400))
345 | now="$(date +%s)"
346 | not_after="$(echo "$cert" | openssl x509 -noout -enddate | head -n1 | cut -d= -f2 | tr '\n' '\0' | xargs -0I{} date -d '{}' +%s)"
347 | cutoff=$((not_after - renewal_period))
348 |
349 | info "Now: $(date -I -d "@$now")"
350 | info "Not after: $(date -I -d "@$not_after")"
351 | info "Renews at: $(date -I -d "@$((cutoff+1))")"
352 |
353 | if [ "$now" -gt "$cutoff" ]; then
354 | return 0
355 | else
356 | return 1
357 | fi
358 | }
359 |
360 | # Returns 0 if $1 is on or after now.
361 | # @param $1 the date, in YYYY-MM-DD format
362 | is_valid_on() {
363 | local start_date="$1"
364 | if ! [[ "$start_date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
365 | warn "Invalid date: $start_date"
366 | return 1
367 | fi
368 |
369 | local start now
370 | start="$(date -d "$1" +%s)"
371 | now="$(date +%s)"
372 | if [ "$now" -ge "$start" ]; then
373 | return 0
374 | else
375 | return 1
376 | fi
377 | }
378 |
379 | # Calls a store init hook. Checks that we are unable to write to it first.
380 | # $1: The name of the variable to use for the call.
381 | # $2: --source to source it, --exec to exec it.
382 | call_store_init_hook() (
383 | if [ $# -ne 2 ]; then
384 | error "Usage: [--source|--exec] VARIABLE_NAME"
385 | return 1
386 | fi
387 |
388 | local source=0
389 | case "$1" in
390 | --source)
391 | source=1
392 | ;;
393 | --exec)
394 | source=0
395 | ;;
396 | *)
397 | error "--source or --exec must be provided"
398 | return 1
399 | ;;
400 | esac
401 |
402 | local name="$2"
403 | if [[ -v "$name" ]] && [ -f "${!name}" ] && [ -x "${!name}" ]; then
404 | if [ -w "${!name}" ] || touch "${!name}" &>/dev/null; then
405 | error "$name (${!name}) was executable and writeable, aborting!"
406 | return 1
407 | else
408 | mkdir -p "$NIXPKCS_STORE_DIR"
409 |
410 | if [ $source -eq 0 ]; then
411 | exec "${!name}"
412 | else
413 | # shellcheck disable=SC1090
414 | . "${!name}"
415 | fi
416 |
417 | return 0
418 | fi
419 | fi
420 | )
421 |
422 | # Initializes the store if we need to.
423 | maybe_init_store() {
424 | if [[ -v NIXPKCS_STORE_DIR ]] && [ -n "$NIXPKCS_STORE_DIR" ]; then
425 | info "Using store directory: $NIXPKCS_STORE_DIR"
426 | if [ ! -d "$NIXPKCS_STORE_DIR" ]; then
427 | # Get rid of the trailing newline on the PIN files, and make sure we own them.
428 | # The permissions should be fairly restrictive by default.
429 | local owner
430 | owner="$(id -u):$(id -g)"
431 | if [ -n "$key_options_so_pin" ] \
432 | && [ -n "$key_options_so_pin_file" ] && [ -f "$key_options_so_pin_file" ]; then
433 | info "Using SO PIN file: $key_options_so_pin_file"
434 | log_exec -- truncate -s0 "$key_options_so_pin_file"
435 | log_exec -- chown "$owner" "$key_options_so_pin_file"
436 | log_exec -- chmod 0600 "$key_options_so_pin_file"
437 | echo -n "$key_options_so_pin" >> "$key_options_so_pin_file" || true
438 | fi
439 |
440 | if [ -n "$cert_options_user_pin" ] \
441 | && [ -n "$cert_options_user_pin_file" ] \
442 | && [ -f "$cert_options_user_pin_file" ] \
443 | && [ "$(readlink -- "$cert_options_user_pin_file")" != "$(readlink -- "$key_options_so_pin_file")" ]; then
444 | info "Using User PIN file: $cert_options_user_pin_file"
445 | log_exec -- truncate -s0 "$cert_options_user_pin_file"
446 | log_exec -- chown "$owner" "$cert_options_user_pin_file"
447 | log_exec -- chmod 0600 "$cert_options_user_pin_file"
448 | echo -n "$cert_options_user_pin" >> "$cert_options_user_pin_file" || true
449 | fi
450 |
451 | info "Initializing store."
452 |
453 | # Run the one defined in the module.
454 | call_store_init_hook --source NIXPKCS_STORE_INIT
455 |
456 | # And any defined by the user.
457 | call_store_init_hook --exec NIXPKCS_STORE_INIT_HOOK
458 |
459 | info "Store initialized successfully."
460 | fi
461 | fi
462 | }
463 |
464 | # Runs the rekey hook if it exists with the specified arguments.
465 | _REKEY_STATUS=0
466 | run_rekey_hook() {
467 | _REKEY_STATUS=0
468 | if [ -n "$cert_options_rekey_hook" ] && [ -x "$cert_options_rekey_hook" ]; then
469 | info "Running rekey hook: $cert_options_rekey_hook"
470 | set +e
471 | echo "$_CERT" | "$cert_options_rekey_hook" "$@"
472 | _REKEY_STATUS=$?
473 | set -e
474 | fi
475 | }
476 |
477 | info "Starting."
478 |
479 | # Make sure we have the list of params we're reading ahead of time.
480 | declare token id uri \
481 | key_options_algorithm key_options_type key_options_so_pin_file key_options_soft_fail key_options_force key_options_destroy_old key_options_login_as_user \
482 | cert_options_digest cert_options_serial cert_options_subject \
483 | cert_options_valid_starting cert_options_validity_days cert_options_renewal_period cert_options_user_pin_file cert_options_rekey_hook
484 |
485 | vars=(token id uri
486 | key_options_algorithm key_options_type key_options_so_pin_file key_options_soft_fail key_options_force key_options_destroy_old key_options_login_as_user
487 | cert_options_digest cert_options_serial cert_options_subject
488 | cert_options_valid_starting cert_options_validity_days cert_options_renewal_period cert_options_user_pin_file cert_options_rekey_hook
489 | )
490 |
491 | {
492 | # Read them from jq.
493 | for var in ${vars[@]+"${vars[@]}"}; do
494 | IFS= read -r "${var?}"
495 | debug "$var=${!var}"
496 | done
497 |
498 | # Check the required params.
499 | for required_param in "$token" "$id" "$uri" \
500 | "$key_options_algorithm" "$key_options_type" \
501 | "$cert_options_digest" "$cert_options_subject" \
502 | "$cert_options_validity_days" "$cert_options_renewal_period"; do
503 | if [ -z "$required_param" ]; then
504 | error "Required parameter missing"
505 | exit 1
506 | fi
507 | done
508 |
509 | # Validate the algorithm and type.
510 | key_options_algorithm="$(echo "$key_options_algorithm" | tr '[:lower:]' '[:upper:]')"
511 | key_options_type="$(echo "$key_options_type" | tr '[:upper:]' '[:lower:]')"
512 | case "$key_options_algorithm" in
513 | RSA)
514 | case "$key_options_type" in
515 | 2048|3072|4096)
516 | ;;
517 | *)
518 | error "Invalid RSA bits: $key_options_type"
519 | exit 1
520 | ;;
521 | esac
522 | ;;
523 | EC)
524 | case "$key_options_type" in
525 | secp256r1|prime256r1|secp384r1|secp521r1)
526 | ;;
527 | ed25519|curve25519)
528 | ;;
529 | *)
530 | error "Invalid EC curve: $key_options_type"
531 | exit 1
532 | ;;
533 | esac
534 | ;;
535 | *)
536 | error "Invalid key algorithm: $key_options_algorithm"
537 | exit 1
538 | ;;
539 | esac
540 |
541 | # Validate the digest against those allowed by openssl.
542 | cert_options_digest="$(echo "$cert_options_digest" | tr '[:upper:]' '[:lower:]')"
543 | if ! { openssl list -1 --digest-commands | grep -q "^${cert_options_digest}$"; }; then
544 | error "Invalid digest: $cert_options_digest. Use 'openssl list --digest-commands' for a list."
545 | exit 1
546 | fi
547 |
548 | # Read the pin file(s).
549 | read_secret_file "$key_options_so_pin_file"
550 | key_options_so_pin="$_SECRET"
551 | read_secret_file "$cert_options_user_pin_file"
552 | cert_options_user_pin="$_SECRET"
553 | } < <(
554 | # Ensure that these are ordered the same as above.
555 | echo "$NIXPKCS_KEY_SPEC" | \
556 | jq --arg expectedLength "${#vars[@]}" -r '
557 | def unpack_exact_n($arr; $length):
558 | if ($arr | length) == $length then $arr | .[]
559 | else error("invalid length, expected " + ($length | tostring) + " but got " + ($arr | length | tostring))
560 | end;
561 | unpack_exact_n([
562 | .token? // "", .id? // 0, .uri? // "",
563 | .keyOptions?.algorithm? // "", .keyOptions?.type? // "", .keyOptions?.soPinFile? // "", .keyOptions?.softFail // false, .keyOptions?.force // false, .keyOptions?.destroyOld // false, .keyOptions?.loginAsUser // false,
564 | .certOptions?.digest? // "SHA256", .certOptions?.serial? // "", .certOptions.subject? // "",
565 | .certOptions?.validStarting? // "", .certOptions?.validityDays? // 0,
566 | .certOptions?.renewalPeriod? // 0, .certOptions?.pinFile? // "", .certOptions?.rekeyHook? // ""
567 | ]; $expectedLength | tonumber)
568 | '
569 | )
570 |
571 | # Read the key usage.
572 | declare -a key_options_usage_arr
573 | declare -A key_options_usage
574 | { mapfile -t key_options_usage_arr; } < <(
575 | echo "$NIXPKCS_KEY_SPEC" | \
576 | jq -r '(.keyOptions?.usage? // []).[]'
577 | )
578 | for usage in ${key_options_usage_arr[@]+"${key_options_usage_arr[@]}"}; do
579 | key_options_usage["$usage"]=1
580 | done
581 | debug "key_options_usage=${!key_options_usage[*]}"
582 |
583 | # Read the extensions.
584 | declare -a cert_options_extensions
585 | { mapfile -t cert_options_extensions;
586 | debug "cert_options_extensions=${cert_options_extensions[*]}"
587 | } < <(
588 | echo "$NIXPKCS_KEY_SPEC" | \
589 | jq -r '(.certOptions?.extensions? // []).[]'
590 | )
591 |
592 | failure=1
593 | if [ "$key_options_soft_fail" == 'true' ]; then
594 | failure=0
595 | fi
596 |
597 | # Initialize the store.
598 | maybe_init_store
599 |
600 | # Ensure that the token is present.
601 | if ! probe_token; then
602 | warn "Token is not present; exiting with status $failure."
603 | exit $failure
604 | fi
605 |
606 | # Read the certificate and run the rekey hook with the old cert.
607 | if [ "$key_options_force" != 'true' ]; then
608 | read_cert
609 | if [ -n "$_CERT" ]; then
610 | echo "$_CERT" | run_rekey_hook "$label" old
611 |
612 | if [ $_REKEY_STATUS -ne 0 ]; then
613 | warn "Rekey hook returned $_REKEY_STATUS; skipping rekey."
614 | fi
615 | fi
616 | fi
617 |
618 | # Check if we need to regenerate the key.
619 | if [ $_REKEY_STATUS -eq 0 ] && { [ -z "$_CERT" ] || is_expiring "$_CERT" "$cert_options_renewal_period"; } \
620 | && { [ -z "$cert_options_valid_starting" ] || is_valid_on "$cert_options_valid_starting"; }; then
621 | probe_token
622 | gen_key
623 | read_cert
624 | if [ -n "$_CERT" ]; then
625 | if is_expiring "$_CERT" "$cert_options_renewal_period"; then
626 | warn "Generated a cert that's about to expire!"
627 | fi
628 |
629 | echo "$_CERT" | run_rekey_hook "$label" new
630 |
631 | if [ $_REKEY_STATUS -ne 0 ]; then
632 | warn "Rekey hook returned $_REKEY_STATUS."
633 | fi
634 | fi
635 | fi
636 |
637 | if [ -n "$_CERT" ]; then
638 | echo "$_CERT"
639 | exit 0
640 | elif [ -n "$cert_options_valid_starting" ] && ! is_valid_on "$cert_options_valid_starting"; then
641 | warn "Token is present but we are not yet ready to generate the key (waiting until $cert_options_valid_starting). Exiting."
642 | exit 0
643 | else
644 | error "No certificate found! nixpkcs may have failed to run."
645 | exit 2
646 | fi
647 |
--------------------------------------------------------------------------------
/overlay.nix:
--------------------------------------------------------------------------------
1 | { self, lib }:
2 |
3 | final: prev:
4 | let
5 | # Creates an attrset mapping package names to that package with the given PKCS#11 module.
6 | mkPkcs11Consumers =
7 | package:
8 | let
9 | pkcs11Consumers = [
10 | "nixpkcs"
11 | "opensc"
12 | "openssl"
13 | ];
14 | in
15 | lib.listToAttrs (
16 | map (name: lib.nameValuePair name (final.${name}.withPkcs11Module package)) pkcs11Consumers
17 | );
18 |
19 | # Wraps a package with a symlink join that respects overrides.
20 | symlinkJoinWith =
21 | {
22 | package,
23 | pkcs11Module,
24 | moduleEnv ? { },
25 | passthru ? { },
26 | extraWrapProgramArgs ? [ ],
27 | findDirectory ? "bin",
28 | extraFindArgs ? [ ],
29 | }@args:
30 | final.symlinkJoin {
31 | name = "${package.pname}-with-pkcs11";
32 | paths = [ package ];
33 | buildInputs = [ final.makeWrapper ];
34 | postBuild = ''
35 | args=(${
36 | lib.escapeShellArgs (
37 | lib.flatten (
38 | lib.mapAttrsToList (name: value: [
39 | "--set-default"
40 | name
41 | value
42 | ]) (pkcs11Module.mkEnv moduleEnv)
43 | )
44 | )
45 | })
46 | args+=(${lib.escapeShellArgs extraWrapProgramArgs})
47 | find -L $out/${lib.escapeShellArg findDirectory} -type f ${lib.escapeShellArgs extraFindArgs} | while read program; do
48 | wrapProgram "$program" "''${args[@]}"
49 | done
50 | '';
51 | passthru = {
52 | # If the resulting package is overridden, use the symlinkJoinWith wrapper.
53 | # (nginx needs this, as an example)
54 | inherit (package) pname version;
55 | override =
56 | attrs:
57 | symlinkJoinWith (
58 | args
59 | // {
60 | package = package.override attrs;
61 | }
62 | );
63 | }
64 | // passthru;
65 | };
66 | in
67 | {
68 | ### PATCHES ###
69 | nebula =
70 | if lib.findFirst (x: x == "pkcs11") null (prev.nebula.tags or [ ]) == null then
71 | if lib.versionOlder prev.nebula.version "1.10.0" then
72 | let
73 | version = "${prev.nebula.version}-pkcs11";
74 | patchedSrc = final.stdenv.mkDerivation {
75 | name = "nebula-${version}-patched";
76 | inherit (prev.nebula) src;
77 | patches = [
78 | (final.fetchpatch {
79 | url = "https://github.com/slackhq/nebula/commit/35603d1c39fa8bfb0d35ef7ee29716023d0c65c0.patch";
80 | hash = "sha256-uTE+us+9mH45iBrR0MhH5bFzMSzzyjCitKLJiVpTMR0=";
81 | })
82 | ];
83 | phases = [
84 | "unpackPhase"
85 | "patchPhase"
86 | "installPhase"
87 | ];
88 | installPhase = "cp -Ra . $out";
89 | };
90 | in
91 | (prev.nebula.override {
92 | buildGoModule =
93 | args:
94 | final.buildGoModule (
95 | args
96 | // {
97 | inherit version;
98 | src = patchedSrc;
99 | vendorHash = "sha256-7G7yp6NV+ECz4MRtHRjF6tiHD9Uq2x8s6y4iIfRih/o=";
100 | }
101 | );
102 | }).overrideAttrs
103 | (package: {
104 | inherit version;
105 | src = patchedSrc;
106 | tags = [ "pkcs11" ] ++ (package.tags or [ ]);
107 | })
108 | else
109 | prev.nebula.overrideAttrs (package: {
110 | tags = [ "pkcs11" ] ++ (package.tags or [ ]);
111 | })
112 | else
113 | prev.nebula;
114 |
115 | ### WRAPPERS ###
116 |
117 | nixpkcs = {
118 | name = "nixpkcs";
119 | withPkcs11Module =
120 | { pkcs11Module, ... }:
121 | final.writeShellApplication {
122 | name = "nixpkcs.sh";
123 | runtimeInputs = [
124 | final.util-linux
125 | final.jq
126 | (final.opensc.withPkcs11Module { inherit pkcs11Module; })
127 | (final.openssl.withPkcs11Module { inherit pkcs11Module; })
128 | ];
129 | text = lib.readFile ./nixpkcs.sh;
130 | };
131 | };
132 |
133 | openssl = prev.openssl.overrideAttrs (
134 | finalPackage: previousPackage: {
135 | passthru = (previousPackage.passthru or { }) // {
136 | /**
137 | Creates a symlinkJoin wrapper to run any program with a PKCS#11 module loaded into OpenSSL.
138 | */
139 | withPkcs11Module =
140 | {
141 | # contains two keys: `path` and `options`
142 | pkcs11Module,
143 | # the package whose bin directory to wrap
144 | package ? final.openssl.bin,
145 | # the root config option, may need changing (e.g. to "nodejs_conf" for nodejs)
146 | confName ? "openssl_conf",
147 | # the name for the legacy engine, if enabled
148 | engineName ? "pkcs11",
149 | # true if we should load p11-kit as a legacy engine
150 | enableLegacyEngine ? false,
151 | # extra options for the engine
152 | extraEngineOptions ? { },
153 | # the name for the new-style provider, if enabled
154 | providerName ? "pkcs11",
155 | # true to enable the provider
156 | enableProvider ? true,
157 | # extra options to pass to the provider.
158 | extraProviderOptions ? { },
159 | # environment variables to set
160 | moduleEnv ? { },
161 | # passthru on the symlinkJoin
162 | passthru ? { },
163 | # true to enable debugging
164 | debug ? false,
165 | ...
166 | }:
167 | let
168 | # Adds an ordering prefix to a string.
169 | addOrder = order: str: "${toString order}-${str}";
170 |
171 | # Adds an ordering prefix to all keys in an attrset.
172 | addOrderToAttrs =
173 | order: lib.attrsets.mapAttrs' (name: value: lib.nameValuePair (addOrder order name) value);
174 |
175 | # Strips an ordering prefix from a string.
176 | stripOrder =
177 | str:
178 | let
179 | match = lib.match "([[:digit:]]+-)?(.*)" str;
180 | in
181 | if match != null && lib.length match > 0 then lib.elemAt match ((lib.length match) - 1) else str;
182 |
183 | # The PKCS#11 module options.
184 | moduleOptions = if pkcs11Module == null then { } else pkcs11Module.openSslOptions or { };
185 |
186 | # The PKCS#11 engine options.
187 | engineOptions = {
188 | default_algorithms = "ALL";
189 | }
190 | // extraEngineOptions;
191 |
192 | # The provider options. Defaults to loading provider URLs from PEM files.
193 | providerOptions = {
194 | pkcs11-module-encode-provider-uri-to-pem = true;
195 | }
196 | // lib.optionalAttrs (pkcs11Module != null) {
197 | pkcs11-module-path = "${pkcs11Module.path}";
198 | }
199 | // moduleOptions
200 | // extraProviderOptions;
201 |
202 | # The OpenSSL config.
203 | config =
204 | let
205 | cnfPrefix = "${package.pname}-with-pkcs11";
206 | originalMkKeyValue = lib.generators.mkKeyValueDefault { } " = ";
207 | mkKeyValue =
208 | k: v:
209 | let
210 | stripped = stripOrder k;
211 | normalizedValue = if v == null then "EMPTY" else v;
212 | in
213 | if stripped == ".include" then ".include ${v}" else originalMkKeyValue stripped normalizedValue;
214 | in
215 | final.writeText "${cnfPrefix}.openssl.cnf" (
216 | lib.generators.toINIWithGlobalSection
217 | {
218 | inherit mkKeyValue;
219 | listsAsDuplicateKeys = true;
220 | }
221 | {
222 | globalSection = {
223 | ${addOrder 10 confName} = "openssl_init";
224 | ${addOrder 11 ".include"} = "${prev.openssl.out}/etc/ssl/openssl.cnf";
225 | };
226 |
227 | sections = {
228 | openssl_init =
229 | { }
230 | // (lib.optionalAttrs enableLegacyEngine (addOrderToAttrs 10 { engines = "engine_section"; }))
231 | // (lib.optionalAttrs enableProvider (addOrderToAttrs 11 { providers = "provider_section"; }));
232 | }
233 | // (lib.optionalAttrs enableLegacyEngine {
234 | engine_section = {
235 | ${engineName} = "${engineName}_engine_section";
236 | };
237 | "${engineName}_engine_section" = {
238 | ${addOrder 10 "engine_id"} = engineName;
239 | ${addOrder 11 "dynamic_path"} = "${final.libp11}/lib/engines/libpkcs11.so";
240 | ${addOrder 99 "init"} = 1;
241 | }
242 | // lib.optionalAttrs (debug != null && debug != false) {
243 | ${addOrder 12 "VERBOSE"} = null;
244 | }
245 | // lib.optionalAttrs (pkcs11Module != null) {
246 | ${addOrder 13 "MODULE_PATH"} = pkcs11Module.path;
247 | }
248 | // (addOrderToAttrs 20 engineOptions);
249 | })
250 | // {
251 | provider_section = {
252 | ${addOrder 10 "default"} = "default_provider_section";
253 | }
254 | // (lib.optionalAttrs enableProvider {
255 | ${addOrder 11 providerName} = "${providerName}_provider_section";
256 | });
257 | default_provider_section = (addOrderToAttrs 99 { activate = 1; });
258 | }
259 | // (lib.optionalAttrs enableProvider {
260 | "${providerName}_provider_section" = {
261 | ${addOrder 10 "module"} = "${final.pkcs11-provider}/lib/ossl-modules/pkcs11.so";
262 | ${addOrder 99 "activate"} = 1;
263 | }
264 | // (addOrderToAttrs 20 providerOptions);
265 | });
266 | }
267 | );
268 | providerDebugLevel =
269 | let
270 | realDebug = if debug == true then 2 else debug;
271 | in
272 | if lib.isString realDebug then
273 | realDebug
274 | else if lib.isInt realDebug then
275 | "file:/dev/stderr,level:${toString realDebug}"
276 | else
277 | null;
278 | in
279 | symlinkJoinWith {
280 | inherit
281 | package
282 | pkcs11Module
283 | moduleEnv
284 | passthru
285 | ;
286 | extraWrapProgramArgs = [
287 | "--set"
288 | "OPENSSL_CONF"
289 | config
290 | ]
291 | ++ lib.optionals enableProvider [
292 | "--set"
293 | "PKCS11_PROVIDER_MODULE"
294 | pkcs11Module.path
295 | ]
296 | ++ lib.optionals (enableProvider && providerDebugLevel != null) [
297 | "--set-default"
298 | "PKCS11_PROVIDER_DEBUG"
299 | providerDebugLevel
300 | ];
301 | };
302 | };
303 | }
304 | );
305 |
306 | opensc = prev.opensc.overrideAttrs (
307 | finalPackage: previousPackage: {
308 | passthru = (previousPackage.passthru or { }) // {
309 | pkcs11Module = {
310 | path = "${finalPackage.finalPackage}/lib/opensc-pkcs11.so";
311 | openSslOptions = { };
312 | mkEnv =
313 | {
314 | debug ? 0,
315 | extraEnv ? { },
316 | }:
317 | {
318 | OPENSC_DEBUG = toString debug;
319 | }
320 | // extraEnv;
321 | };
322 |
323 | withPkcs11Module =
324 | {
325 | # the module
326 | pkcs11Module,
327 | # environment variables to set; see .mkEnv
328 | moduleEnv ? { },
329 | # passthrus to add to the symlinkJoin
330 | passthru ? { },
331 | ...
332 | }:
333 | symlinkJoinWith {
334 | package = finalPackage.finalPackage;
335 | inherit pkcs11Module moduleEnv passthru;
336 | extraWrapProgramArgs = [
337 | "--add-flags"
338 | "--module ${pkcs11Module.path}"
339 | ];
340 | extraFindArgs = [
341 | "-name"
342 | "pkcs11-tool"
343 | ];
344 | };
345 | };
346 | }
347 | );
348 |
349 | pkcs11-provider = prev.pkcs11-provider.overrideAttrs (
350 | finalPackage: previousPackage: {
351 | passthru = (previousPackage.passthru or { }) // {
352 | uri2pem = final.stdenv.mkDerivation {
353 | pname = "pkcs11-provider-uri2pem";
354 | inherit (previousPackage) version src;
355 |
356 | buildInputs = [
357 | (final.python3.withPackages (pkgs: lib.singleton pkgs.asn1crypto))
358 | ];
359 |
360 | dontBuild = true;
361 |
362 | installPhase = ''
363 | mkdir -p $out/bin
364 | echo '#!/usr/bin/env python3' > $out/bin/uri2pem
365 | cat tools/uri2pem.py | grep -v '^#!' >> $out/bin/uri2pem
366 | chmod +x $out/bin/uri2pem
367 | '';
368 |
369 | passthru.__functor =
370 | self: uri:
371 | final.runCommand "pkcs11-uri2pem" { inherit uri; } ''
372 | ${self}/bin/uri2pem --out "$out" "$uri"
373 | '';
374 | };
375 | };
376 | }
377 | );
378 |
379 | ### MODULES ###
380 |
381 | # nss is broken for this usecase but nss_latest is not.
382 | # We're only maintaining one of these things.
383 | nss_latest = prev.nss_latest.overrideAttrs (
384 | finalPackage: previousPackage: {
385 | passthru =
386 | (previousPackage.passthru or { })
387 | // {
388 | pkcs11Module = {
389 | path = "${finalPackage.finalPackage}/lib/libsoftokn3.so";
390 | openSslOptions = { };
391 | mkEnv =
392 | {
393 | storeDir ? "/etc/pki/nssdb",
394 | extraEnv ? { },
395 | }:
396 | {
397 | NSS_LIB_PARAMS = "configDir=${storeDir}";
398 | NIXPKCS_STORE_DIR = storeDir;
399 | }
400 | // extraEnv;
401 | storeInit = final.writeShellScript "nss-pkcs11-init" ''
402 | local pin="$cert_options_user_pin"
403 | if [ -z "$pin" ]; then
404 | pin="$key_options_so_pin"
405 | fi
406 | if [ -z "$pin" ]; then
407 | error "User or Security Officer PIN must be set"
408 | return 1
409 | fi
410 | echo -n "$pin" | log_exec -- ${finalPackage.finalPackage.tools}/bin/certutil -N \
411 | -d "$NIXPKCS_STORE_DIR" -f /dev/stdin
412 | '';
413 | };
414 | }
415 | // (mkPkcs11Consumers finalPackage.finalPackage);
416 | }
417 | );
418 |
419 | tpm2-pkcs11 = prev.tpm2-pkcs11.overrideAttrs (
420 | finalPackage: previousPackage: {
421 | passthru =
422 | (previousPackage.passthru or { })
423 | // {
424 | pkcs11Module = {
425 | path = "${finalPackage.finalPackage}/lib/libtpm2_pkcs11.so";
426 | openSslOptions = {
427 | pkcs11-module-quirks = "no-operation-state";
428 | };
429 | mkEnv =
430 | {
431 | storeDir ? "/etc/tpm2-pkcs11",
432 | # error
433 | logLevel ? 0,
434 | disableFapiLogging ? true,
435 | extraEnv ? { },
436 | }:
437 | {
438 | TPM2_PKCS11_STORE = storeDir;
439 | TPM2_PKCS11_BACKEND = "esysdb";
440 | TPM2_PKCS11_LOG_LEVEL = toString logLevel;
441 | NIXPKCS_STORE_DIR = storeDir;
442 | }
443 | // lib.optionalAttrs disableFapiLogging {
444 | TSS2_LOG = "fapi+NONE";
445 | }
446 | // extraEnv;
447 | storeInit = final.writeShellScript "tpm2-pkcs11-init" ''
448 | if [ -z "$key_options_so_pin" ] || [ -z "$cert_options_user_pin" ]; then
449 | error "Security Officer and User PIN must be set to initialize the TPM"
450 | return 1
451 | fi
452 |
453 | # Initialize the TPM.
454 | log_exec -- \
455 | ${finalPackage.finalPackage.bin}/bin/tpm2_ptool init --path="$NIXPKCS_STORE_DIR"
456 | log_exec "$key_options_so_pin" "$cert_options_user_pin" -- \
457 | ${finalPackage.finalPackage.bin}/bin/tpm2_ptool addtoken --pid=1 \
458 | --sopin="$key_options_so_pin" \
459 | --userpin="$cert_options_user_pin" \
460 | --label="$token" --path="$NIXPKCS_STORE_DIR"
461 |
462 | # Ensure permissions on the store directory.
463 | owner="$(id -u)"
464 | log_exec -- chown -R "$owner:tss" "$NIXPKCS_STORE_DIR"
465 | log_exec -- chmod 0770 "$NIXPKCS_STORE_DIR"
466 | log_exec -- find "$NIXPKCS_STORE_DIR" -type f -exec chmod 0660 {} \;
467 |
468 | # Ensure permissions on the SO PIN and user PIN.
469 | if [ -n "$key_options_so_pin_file" ] && [ -f "$key_options_so_pin_file" ]; then
470 | log_exec -- chown "$owner:tss" "$key_options_so_pin_file"
471 | log_exec -- chmod 0640 "$key_options_so_pin_file"
472 | fi
473 | if [ -n "$cert_options_user_pin_file" ] && [ -f "$cert_options_user_pin_file" ]; then
474 | log_exec -- chown "$owner:tss" "$cert_options_user_pin_file"
475 | log_exec -- chmod 0640 "$cert_options_user_pin_file"
476 | fi
477 | '';
478 |
479 | };
480 | }
481 | // (mkPkcs11Consumers finalPackage.finalPackage);
482 | }
483 | );
484 |
485 | yubico-piv-tool = prev.yubico-piv-tool.overrideAttrs (
486 | finalPackage: previousPackage: {
487 | passthru =
488 | (previousPackage.passthru or { })
489 | // {
490 | pkcs11Module = {
491 | path = "${finalPackage.finalPackage}/lib/libykcs11.so";
492 | openSslOptions = {
493 | pkcs11-module-login-behavior = "never";
494 | pkcs11-module-quirks = "no-deinit no-operation-state";
495 | pkcs11-module-cache-pins = "cache";
496 | };
497 | noLabels = true;
498 | mkEnv =
499 | {
500 | debug ? 0,
501 | extraEnv ? { },
502 | }:
503 | {
504 | YKCS11_DBG = toString debug;
505 | }
506 | // extraEnv;
507 | };
508 | }
509 | // (mkPkcs11Consumers finalPackage.finalPackage);
510 | }
511 | );
512 |
513 | yubihsm-shell = prev.yubihsm-shell.overrideAttrs (
514 | finalPackage: previousPackage: {
515 | passthru =
516 | (previousPackage.passthru or { })
517 | // {
518 | pkcs11Module = {
519 | path = "${finalPackage.finalPackage}/lib/pkcs11/yubihsm_pkcs11.so";
520 | openSslOptions = {
521 | pkcs11-module-quirks = "no-deinit";
522 | pkcs11-module-cache-pins = "cache";
523 | };
524 | mkEnv =
525 | {
526 | debug ? false,
527 | libdebug ? false,
528 | dinout ? false,
529 | debug-file ? "stderr",
530 | connector ? "yhusb://",
531 | cacert ? null,
532 | proxy ? null,
533 | timeout ? 5,
534 | extraConf ? { },
535 | extraEnv ? { },
536 | }:
537 | {
538 | # https://docs.yubico.com/hardware/yubihsm-2/hsm-2-user-guide/hsm2-sdk-tools-libraries.html#configuration
539 | YUBIHSM_PKCS11_CONF = toString (
540 | final.writeText "yubihsm_pkcs11.conf" (
541 | lib.generators.toINIWithGlobalSection
542 | {
543 | mkKeyValue =
544 | let
545 | originalMkKeyValue = lib.generators.mkKeyValueDefault { } " = ";
546 | in
547 | k: v:
548 | if v == true then
549 | k
550 | else if v == false || v == null then
551 | ""
552 | else
553 | originalMkKeyValue k v;
554 | }
555 | {
556 | globalSection = {
557 | inherit
558 | debug
559 | libdebug
560 | dinout
561 | debug-file
562 | connector
563 | cacert
564 | proxy
565 | timeout
566 | ;
567 | }
568 | // extraConf;
569 | }
570 | )
571 | );
572 | }
573 | // extraEnv;
574 | };
575 | }
576 | // (mkPkcs11Consumers finalPackage.finalPackage);
577 | }
578 | );
579 | }
580 |
--------------------------------------------------------------------------------
/module.nix:
--------------------------------------------------------------------------------
1 | self:
2 |
3 | {
4 | config,
5 | pkgs,
6 | lib,
7 | ...
8 | }:
9 |
10 | let
11 | inherit (builtins) hashString;
12 |
13 | inherit (lib.trivial) isInt mod toHexString;
14 |
15 | inherit (lib.strings)
16 | isString
17 | typeOf
18 | substring
19 | split
20 | escapeRegex
21 | escapeURL
22 | escapeShellArg
23 | concatStrings
24 | concatStringsSep
25 | replaceStrings
26 | fixedWidthString
27 | toJSON
28 | ;
29 |
30 | inherit (lib.lists)
31 | elemAt
32 | length
33 | filter
34 | singleton
35 | imap0
36 | foldl
37 | ;
38 |
39 | inherit (lib.attrsets)
40 | mapAttrs
41 | mapAttrs'
42 | mapAttrsToList
43 | filterAttrs
44 | optionalAttrs
45 | nameValuePair
46 | ;
47 |
48 | inherit (lib.options) mkOption literalExpression;
49 |
50 | inherit (lib.modules) mkIf mkMerge mkRenamedOptionModule;
51 |
52 | inherit (lib) types;
53 |
54 | # Parses a PKCS#11 id into an integer.
55 | parseId =
56 | keyName: id:
57 | if isInt id && id >= 0 then
58 | id
59 | else if isString id then
60 | (builtins.fromTOML "id=0x${id}").id
61 | else
62 | throw "ID for nixpkcs key '${keyName}' was not an unsigned integer or hex string";
63 |
64 | # Creates a PKCS#11 URI.
65 | mkPkcs11Uri =
66 | {
67 | authority,
68 | # The authority
69 | query ? { }, # The query
70 | }:
71 | let
72 | /*
73 | Converts an integer to a value in a PKCS#11 URI using the following rules:
74 | 0: %00
75 | 1: %01
76 | 10: %0a
77 | 256: %01%00
78 | ...
79 | That is: it's interpreted as a 63-bit positive integer and leading zeroes are stripped.
80 | */
81 | intToUriValue =
82 | value:
83 | let
84 | zero = "%00";
85 |
86 | # Support Nix's full integer width for PKCS#11 IDs.
87 | paddedValue = fixedWidthString 16 "0" (toHexString value);
88 | splitValue = split "([0-9A-F]{2})" paddedValue;
89 | replacedValue = imap0 (
90 | idx: vals:
91 | let
92 | even = mod idx 2 == 0;
93 | val = if even then vals else elemAt vals 0;
94 | splitLength = length splitValue;
95 | in
96 | if even && idx < splitLength - 1 && val == "" then "%" else val
97 | ) splitValue;
98 | deprefixedValue =
99 | let
100 | regexSplit = split "^(${escapeRegex zero})+" (concatStrings replacedValue);
101 | in
102 | elemAt regexSplit (length regexSplit - 1);
103 | in
104 | if deprefixedValue == "" then zero else deprefixedValue;
105 |
106 | # Serializes a string or int to a PKCS#11 value.
107 | serializePkcs11UriValue =
108 | value:
109 | if isInt value && value >= 0 then
110 | intToUriValue value
111 | else if isString value then
112 | escapeURL value
113 | else
114 | throw "only 8-bit ints and strings are supported in PKCS#11 URIs; got '${typeOf value}'";
115 |
116 | # Converts a list of URI attrs to a query string.
117 | toQuery = mapAttrsToList (
118 | name: value:
119 | if value == null then null else (escapeURL name) + "=" + (serializePkcs11UriValue value)
120 | );
121 |
122 | # The authority string. Just a query string joined with ;.
123 | authorityString = concatStringsSep ";" (filter (x: x != null) (toQuery authority));
124 |
125 | # The query string.
126 | queryString =
127 | let
128 | queryValue = concatStringsSep "&" (filter (x: x != null) (toQuery query));
129 | in
130 | if queryValue == "" then "" else "?" + queryValue;
131 | in
132 | "pkcs11:" + authorityString + queryString;
133 |
134 | # The nixpkcs config.
135 | cfg = config.security.pkcs11;
136 | in
137 | {
138 | imports = [
139 | (mkRenamedOptionModule [ "nixpkcs" ] [ "security" "pkcs11" ])
140 | ];
141 |
142 | options = {
143 | security.pkcs11 = {
144 | enable = mkOption {
145 | type = types.bool;
146 | default = false;
147 | description = "Set to true to enable automated key management using nixpkcs.";
148 | };
149 |
150 | pcsc = {
151 | enable = mkOption {
152 | type = types.bool;
153 | default = false;
154 | description = "Set to true to enable PKCS#11 support using pcsc-lite";
155 | };
156 | users = mkOption {
157 | description = "Any users that should be allowed to access pcsc-lite.";
158 | default = [ ];
159 | example = [ "alice" ];
160 | type = types.listOf types.str;
161 | };
162 | };
163 |
164 | tpm2 = {
165 | enable = mkOption {
166 | type = types.bool;
167 | default = false;
168 | description = "Set to true to enable TPM2 support";
169 | };
170 | };
171 |
172 | environment = {
173 | enable = mkOption {
174 | type = types.bool;
175 | default = true;
176 | description = "Set to true to populate the system environment with nixpkcs keypairs' extraEnv.";
177 | };
178 | };
179 |
180 | uri = {
181 | enable = mkOption {
182 | type = types.bool;
183 | default = true;
184 | description = "Set to true to enable the `nixpkcs-uri` command converting keypair names into URIs.";
185 | };
186 |
187 | package = mkOption {
188 | type = types.package;
189 | default = pkgs.writeShellApplication {
190 | name = "nixpkcs-uri";
191 | text = ''
192 | set -euo pipefail
193 |
194 | # Prints a key name and URI as a tab-separated string.
195 | print_key() {
196 | printf '%s\t%s\n' "$1" "''${keys["$1"]}"
197 | }
198 |
199 | declare -A keys=(
200 | ${concatStringsSep "\n " (
201 | mapAttrsToList (name: value: "[${escapeShellArg name}]=${escapeShellArg value.uri}") cfg.keypairs
202 | )}
203 | )
204 |
205 | if [ $# -gt 0 ]; then
206 | # Require that all keys specified exist.
207 | for key in "$@"; do
208 | if [[ ! -v keys["$key"] ]]; then
209 | echo "unknown key '$key'" >&2
210 | exit 1
211 | fi
212 | done
213 |
214 | if [ $# -gt 1 ]; then
215 | # Print them all out with the requested names.
216 | for key in "$@"; do
217 | print_key "$key"
218 | done
219 | else
220 | # Just print the URI if one is provided.
221 | echo "''${keys["$1"]}"
222 | fi
223 | else
224 | # Print all the key names and URIs if none are specified.
225 | for key in "''${!keys[@]}"; do
226 | print_key "$key"
227 | done
228 | fi
229 | '';
230 | };
231 | description = "Override the nixpkcs-uri package, used to convert key names into PKCS#11 URIs.";
232 | };
233 | };
234 |
235 | keypairs = mkOption {
236 | description = "Keypairs to let nixpkcs manage on this host.";
237 | default = { };
238 | type = types.attrsOf (
239 | types.submodule (
240 | { name, config, ... }:
241 | {
242 | options =
243 | let
244 | authority = {
245 | inherit (config) token;
246 | id = parseId name config.id;
247 | slot-id = if config.slot == null then null else toString config.slot;
248 | type = "private";
249 | }
250 | // optionalAttrs (!(config.pkcs11Module.noLabels or false)) {
251 | # Only supply this for tokens that support labels.
252 | object = name;
253 | };
254 | query = optionalAttrs (config.certOptions.pinFile != null) {
255 | pin-source = "file:${config.certOptions.pinFile}";
256 | };
257 | in
258 | {
259 | enable = mkOption {
260 | type = types.bool;
261 | default = true;
262 | description = "Set to true to disable this key.";
263 | };
264 |
265 | pkcs11Module = mkOption {
266 | type = types.attrs;
267 | description = "The PKCS#11 module to use for this key.";
268 | example = literalExpression ''
269 | inherit (pkgs.yubico-piv-tool) pkcs11Module;
270 | '';
271 | };
272 |
273 | storeInitHook = mkOption {
274 | type = with types; nullOr path;
275 | default = pkgs.writeShellScript "default-store-init-hook" ":";
276 | description = ''
277 | Run the given script after the store is initialized and before nixpkcs runs.
278 |
279 | This script has NIXPKCS_STORE_DIR exported to it.
280 |
281 | This script also always has access to a wrapped OpenSSL and pkcs11-tool on its PATH, in addition to jq.
282 | Returning nonzero from this script aborts nixpkcs.
283 | '';
284 | example = literalExpression ''
285 | pkgs.writeShellScript "store-init-hook" '''
286 | chown -R alice:users "$NIXPKCS_STORE_DIR"
287 | '''
288 | '';
289 | };
290 |
291 | token = mkOption {
292 | type = types.str;
293 | default = "nixpkcs";
294 | description = "The token label.";
295 | example = "YubiKey PIV #123456";
296 | };
297 |
298 | id = mkOption {
299 | type = with types; either ints.unsigned (strMatching "^[0-9a-fA-F]{1,16}$");
300 | description = "The PKCS#11 key ID.";
301 | example = 42;
302 | };
303 |
304 | slot = mkOption {
305 | type = with types; nullOr ints.u8;
306 | default = null;
307 | description = "The PKCS#11 slot ID. Not always required, but may be in some cases.";
308 | example = 42;
309 | };
310 |
311 | uri = mkOption {
312 | type = types.str;
313 | default = mkPkcs11Uri {
314 | inherit authority;
315 | query = query // {
316 | module-path = config.pkcs11Module.path;
317 | };
318 | };
319 | description = "Overrides the PKCS#11 URI.";
320 | example = "pkcs11:token=YubiKey%20PIV%20%23123456;id=%05;type=private";
321 | };
322 |
323 | rfc7512Uri = mkOption {
324 | type = types.str;
325 | default = mkPkcs11Uri {
326 | inherit authority query;
327 | };
328 | description = "Overrides the PKCS#11 URI for applications that strictly follow the RFC.";
329 | example = "pkcs11:token=YubiKey%20PIV%20%23123456;id=%05;type=private";
330 | };
331 |
332 | extraEnv = mkOption {
333 | type = with types; attrsOf str;
334 | default = config.pkcs11Module.mkEnv { };
335 | description = "Extra environment variables to pass to this key's systemd unit";
336 | example = literalExpression ''
337 | { NSS_LIB_PARAMS = "configDir=/etc/softokn"; }
338 | '';
339 | };
340 |
341 | debug = mkOption {
342 | type = types.bool;
343 | default = false;
344 | description = "Set to true to output verbose debugging messages for this key.";
345 | example = true;
346 | };
347 |
348 | keyOptions = {
349 | algorithm = mkOption {
350 | type = types.str;
351 | default = "EC";
352 | description = "The key algorithm (EC, RSA).";
353 | example = "EC";
354 | };
355 |
356 | type = mkOption {
357 | type = types.str;
358 | default = "secp384r1";
359 | description = "The type of key to generate. Algorithm specific.";
360 | example = "secp256r1";
361 | };
362 |
363 | usage = mkOption {
364 | type = with types; listOf str;
365 | default = [
366 | "sign"
367 | "derive"
368 | ];
369 | description = "The key usage. An array of sign|derive|decrypt|wrap.";
370 | example = literalExpression ''
371 | ["sign" "derive" "decrypt"]
372 | '';
373 | };
374 |
375 | soPinFile = mkOption {
376 | type = with types; nullOr path;
377 | default = null;
378 | description = "The file containing the security officer PIN.";
379 | example = "/etc/nixpkcs/so.pin";
380 | };
381 |
382 | softFail = mkOption {
383 | type = types.bool;
384 | default = false;
385 | description = ''
386 | If set to true, exit with success if we can't check the key for renewal.
387 | This is useful for removable devices like yubikeys.
388 | '';
389 | example = true;
390 | };
391 |
392 | force = mkOption {
393 | type = types.bool;
394 | default = false;
395 | description = "Regenerate the key every time. This is dangerous, and is disabled by default.";
396 | example = true;
397 | };
398 |
399 | destroyOld = mkOption {
400 | type = types.bool;
401 | default = false;
402 | description = "Destroy the old key on the token when generating a new key. Defaults to false.";
403 | example = true;
404 | };
405 |
406 | loginAsUser = mkOption {
407 | type = types.bool;
408 | default = true;
409 | description = "Some tokens use the user login for key generation, and the SO login for personalization. If set, this will log in as the 'user' instead.";
410 | example = true;
411 | };
412 | };
413 |
414 | certOptions = {
415 | digest = mkOption {
416 | type = types.str;
417 | default = "SHA256";
418 | description = "The digest to use for this certificate.";
419 | example = "SHA384";
420 | };
421 |
422 | serial = mkOption {
423 | type = with types; nullOr str;
424 | default = null;
425 | description = "The serial to use for this certificate. Set to null to autogenerate. This should be a hex string, not decimal.";
426 | example = "09f91102";
427 | };
428 |
429 | validStarting = mkOption {
430 | type = with types; nullOr (strMatching "^[0-9]{4}-[0-9]{2}-[0-9]{2}$");
431 | default = null;
432 | description = "The date that this key should be generated, in YYYY-MM-DD format. Default today.";
433 | example = "2025-12-25";
434 | };
435 |
436 | validityDays = mkOption {
437 | type = types.ints.positive;
438 | default = 365;
439 | description = "The number of days that this cert should be valid for.";
440 | example = 365 * 3;
441 | };
442 |
443 | renewalPeriod = mkOption {
444 | type = types.int;
445 | default = -1;
446 | description = "The number of days before expiration that this certificate should be renewed. Set to -1 to disable auto-renewal.";
447 | example = 14;
448 | };
449 |
450 | subject = mkOption {
451 | type = types.str;
452 | default = "O=NixOS/CN=nixpkcs Certificate";
453 | description = "The subject to use for this certificate.";
454 | example = "C=US/ST=California/L=Carlsbad/O=nixpkcs/CN=nixpkcs Example CA";
455 | };
456 |
457 | extensions = mkOption {
458 | type = with types; listOf str;
459 | default = [ ];
460 | description = ''
461 | Extensions to add. See OpenSSL documentation for the syntax.
462 | If a `key=value` formatted item is provided, will add it using `-addext`.
463 | Otherwise, adds it using `-extensions`.
464 | '';
465 | example = literalExpression ''
466 | ["v3_ca" "keyUsage=critical,nonRepudiation,keyCertSign,digitalSignature,cRLSign"]
467 | '';
468 | };
469 |
470 | pinFile = mkOption {
471 | type = with types; nullOr path;
472 | default = null;
473 | description = "The file containing the user PIN.";
474 | example = "/etc/nixpkcs/user.pin";
475 | };
476 |
477 | writeTo = mkOption {
478 | type = with types; nullOr path;
479 | default = null;
480 | description = "Write the certificate to this path whenever we regenerate it. Overridden by manually setting rekeyHook.";
481 | example = "/home/alice/cert.crt";
482 | };
483 |
484 | rekeyHook = mkOption {
485 | type = with types; nullOr path;
486 | default =
487 | if config.certOptions.writeTo == null then
488 | null
489 | else
490 | pkgs.writeShellScript "default-rekey-hook" ''
491 | cat > ${escapeShellArg config.certOptions.writeTo}
492 | '';
493 | description = ''
494 | Run the given script whenever nixpkcs runs. The certificate is passed in on stdin.
495 | NIXPKCS_KEY_SPEC is passed in as an environment variable, containing the NixOS module options.
496 | You may use this to restart services when keys change.
497 |
498 | - $1 is set to the name of the key.
499 | - $2 is set to 'old' or 'new' depending on whether the certificate is the old or new one.
500 | You may use this to do something with the certificate when it's checked or when it's renewed.
501 |
502 | This script always has access to a wrapped OpenSSL and pkcs11-tool on its PATH, in addition to jq.
503 | Returning nonzero from this script aborts rekey but returns normally.
504 | '';
505 | example = literalExpression ''
506 | pkgs.writeShellScript "rekey-hook" '''
507 | if [ "$2" == 'new' ]; then
508 | cat > /home/alice/cert.crt
509 | chown alice:alice /home/alice/cert.crt
510 | fi
511 | '''
512 | '';
513 | };
514 | };
515 | };
516 | }
517 | )
518 | );
519 | };
520 | };
521 | };
522 |
523 | config =
524 | let
525 | enabledKeypairs = if cfg.enable then filterAttrs (_: value: value.enable) cfg.keypairs else [ ];
526 | in
527 | mkMerge [
528 | (mkIf cfg.enable {
529 | environment = {
530 | # Place the nixpkgs-uri package on PATH.
531 | systemPackages = mkIf cfg.uri.enable (singleton cfg.uri.package);
532 |
533 | # Automatically set environment variables for the specified keypairs.
534 | variables = mkIf cfg.environment.enable (
535 | foldl (s: x: s // x) { } (mapAttrsToList (name: value: value.extraEnv) enabledKeypairs)
536 | );
537 | };
538 |
539 | systemd.services = mapAttrs' (
540 | name: value:
541 | nameValuePair "nixpkcs@${name}" {
542 | description = "nixpkcs service for key '${name}'";
543 | startAt = "*-*-* 00:00:00";
544 | wants = [ "basic.target" ];
545 | after = [
546 | "basic.target"
547 | "multi-user.target"
548 | ];
549 | wantedBy = [ "multi-user.target" ];
550 | environment =
551 | let
552 | # Escapes systemd %-specifiers in the given value.
553 | escapeSpecifiers = value: replaceStrings [ "%" ] [ "%%" ] (toString value);
554 | in
555 | mapAttrs (_: envValue: (escapeSpecifiers envValue)) (
556 | value.extraEnv
557 | // {
558 | NIXPKCS_KEY_SPEC =
559 | let
560 | filteredValue = {
561 | id = parseId name value.id;
562 |
563 | # We don't want to pass the whole PKCS#11 module here; this should be enough.
564 | inherit (value)
565 | token
566 | uri
567 | keyOptions
568 | certOptions
569 | ;
570 | };
571 | in
572 | toJSON filteredValue;
573 | NIXPKCS_LOCK_FILE =
574 | let
575 | # Come up with a lockfile key. We want to avoid concurrently running two instances
576 | # of the script that use the same PKCS#11 module since there will frequently be
577 | # connections to hardware involved. Better safe than sorry with hardware and
578 | # cryptographic keying, and this involves both. So just use the Nix store path
579 | # of the PKCS#11 token library we are using.
580 | pkcs11ModuleKey = substring 0 8 (hashString "sha256" value.pkcs11Module.path);
581 | lockfileKey = "nixpkcs-${pkcs11ModuleKey}.lock";
582 | in
583 | "/var/lock/${lockfileKey}";
584 | }
585 | // optionalAttrs ((value.pkcs11Module.storeInit or null) != null) {
586 | NIXPKCS_STORE_INIT = value.pkcs11Module.storeInit;
587 | }
588 | // optionalAttrs (value.storeInitHook != null) {
589 | NIXPKCS_STORE_INIT_HOOK = value.storeInitHook;
590 | }
591 | // optionalAttrs (value.pkcs11Module.noLabels or false) {
592 | # For, e.g. Yubikeys, which don't support them.
593 | NIXPKCS_NO_LABELS = 1;
594 | }
595 | // optionalAttrs value.debug {
596 | NIXPKCS_DEBUG = 1;
597 | }
598 | );
599 | serviceConfig = {
600 | Type = "oneshot";
601 | ExecStart = "@${pkgs.nixpkcs.withPkcs11Module value}/bin/nixpkcs.sh nixpkcs ${name}";
602 | };
603 | }
604 | ) enabledKeypairs;
605 | })
606 | (mkIf cfg.pcsc.enable {
607 | services.pcscd.enable = true;
608 | })
609 | (mkIf (cfg.pcsc.enable && length cfg.pcsc.users > 0) {
610 | environment.systemPackages =
611 | let
612 | pcscPolkitRule = pkgs.writeTextDir "share/polkit-1/rules.d/10-pcsc.rules" ''
613 | var users = ${toJSON cfg.pcsc.users};
614 | polkit.addRule(function (action, subject) {
615 | if (action.id === "org.debian.pcsc-lite.access_pcsc" ||
616 | action.id === "org.debian.pcsc-lite.access_card") {
617 | for (var idx = 0; idx < users.length; idx++) {
618 | if (subject.user === users[idx]) {
619 | return polkit.Result.YES;
620 | }
621 | }
622 | return polkit.Result.NO;
623 | }
624 | });
625 | '';
626 | in
627 | singleton pcscPolkitRule;
628 | })
629 | (mkIf cfg.tpm2.enable {
630 | security.tpm2 = {
631 | enable = true;
632 | abrmd.enable = true;
633 | };
634 | })
635 | ];
636 | }
637 |
--------------------------------------------------------------------------------