├── .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 | --------------------------------------------------------------------------------