{pkgs, lib, config, ...}: let inherit (builtins) toString; inherit (lib) types; inherit (config.services) x509; when = x: y: if x == null then "" else y; in { options.services.x509 = { enable = lib.mkEnableOption "Generation of X.509 material"; scheme = lib.mkOption { type = types.enum [ "manual" "self-signed" "letsencrypt" ]; default = "self-signed"; description = '' Scheme for X.509 material. manual: Use certificate and key manually copied on the target at certFile and keyFile. self-signed: Generate a self-signed certificate. letsencrypt: Generate a certificate signed by Let's Encrypt. ''; }; dir = lib.mkOption { type = types.string; default = "/var/x509"; description = ''In "self-signed" scheme: directory of the certificate and key.''; }; certFile = lib.mkOption { type = types.path; example = "/var/x509/cert.pem"; description = ''In "manual" scheme: location of the certificate''; }; keyFile = lib.mkOption { type = types.path; example = "/var/x509/key.pem"; description = ''In "manual" scheme: location of the key file.''; }; dh = lib.mkOption { type = types.int; default = 4096; description = ''Bit size of Diffie–Hellman parameters.''; }; host = lib.mkOption { type = types.nullOr types.string; default = config.networking.domain; }; distributionHost = lib.mkOption { type = types.string; default = config.networking.domain; }; domains = lib.mkOption { type = with types; listOf string; example = [ "example.coop" ]; default = []; }; days = lib.mkOption { type = types.int; default = 3650; }; keySize = lib.mkOption { type = types.int; default = 4096; }; cert = lib.mkOption { type = types.string; default = if x509.scheme == "manual" then x509.certFile else if x509.scheme == "self-signed" then "${x509.dir}/${x509.host}.cert.self-signed.pem" else if x509.scheme == "letsencrypt" then "/var/lib/acme/${x509.host}/fullchain.pem" else throw ''Error: Certificate Scheme must be in [ "manual" "self-signed" "letsencrypt" ]''; }; key = lib.mkOption { type = types.string; default = if x509.scheme == "manual" then x509.keyFile else if x509.scheme == "self-signed" then "${x509.dir}/${x509.host}.key.pem" else if x509.scheme == "letsencrypt" then "/var/lib/acme/${x509.host}/key.pem" else throw ''Error: Certificate Scheme must be in [ "manual" "self-signed" "letsencrypt" ]''; }; opensslConf = lib.mkOption { type = (with types; attrsOf string); default = x509.opensslConf_common // x509.opensslConf_self-signed // x509.opensslConf_auth-signed // x509.opensslConf_user-cert; apply = cnf: pkgs.writeText "openssl.conf" (lib.concatStrings (lib.mapAttrsToList (title: body: (if title == "" then "" else "[ ${title} ]\n") + body) cnf)); }; opensslConf_common = lib.mkOption { type = (with types; attrsOf string); default = { "" = '' RANDFILE = /var/x509/openssl.rand oid_section = extra_oids ''; extra_oids = '' # NOTE: only useful for Extended Validation (EV) jurisdictionOfIncorporationLocalityName = 1.3.6.1.4.1.311.60.2.1.1 jurisdictionOfIncorporationStateOrProvinceName = 1.3.6.1.4.1.311.60.2.1.2 jurisdictionOfIncorporationCountryName = 1.3.6.1.4.1.311.60.2.1.3 ''; req = '' prompt = no distinguished_name = distinguished_name string_mask = pkix #x509_extensions = root_extensions #req_extensions = extension #attributes = req_attributes ''; distinguished_name = '' commonName = ${x509.host} #countryName = #stateOrProvinceName = #localityName = #"0.organizationName" = #organizationalUnitName = #businessCategory = #jurisdictionOfIncorporationLocalityName = $stateOrProvinceName #jurisdictionOfIncorporationStateOrProvinceName = $stateOrProvinceName #jurisdictionOfIncorporationCountryName = $countryName ''; }; }; opensslConf_self-signed = lib.mkOption { type = (with types; attrsOf string); default = { self_signed_extensions = '' basicConstraints = critical,CA:TRUE,pathlen:0 keyUsage = keyCertSign,cRLSign,digitalSignature,keyEncipherment subjectAltName = ${lib.concatMapStringsSep "," (dom: "DNS:${dom}") x509.domains} subjectKeyIdentifier = hash issuerAltName = issuer:copy authorityKeyIdentifier = keyid:always,issuer:always authorityInfoAccess = caIssuers;URI:http://${x509.distributionHost}/x509/${x509.host}.cert.pem crlDistributionPoints = URI:http://${x509.distributionHost}/x509/${x509.host}.crl.self-signed.pem ''; self_signed_ca = '' dir = ${x509.dir}/pub private_key = $dir/sec/key.pem crl_dir = $dir crlnumber = $dir/crl.self-signed.num crl = $dir/crl.self-signed.pem database = $dir/idx.self-signed.txt ''; }; }; opensslConf_auth-signed = lib.mkOption { type = (with types; attrsOf string); default = { auth_signed_extensions = '' basicConstraints = critical,CA:FALSE keyUsage = digitalSignature,keyEncipherment subjectAltName = ${lib.concatMapStringsSep "," (dom: "DNS:${dom}") x509.domains} subjectKeyIdentifier = hash issuerAltName = issuer:copy authorityKeyIdentifier = keyid:always,issuer:always authorityInfoAccess = caIssuers;URI:http://${x509.distributionHost}/x509/${x509.host}.cert.pem crlDistributionPoints = URI:http://${x509.distributionHost}/x509/${x509.host}.crl.pem certificatePolicies = @certificate_policies ''; certificate_policies = '' policyIdentifier = 1.2.250.1.42 CPS.1 = https://${x509.host}/x509/cps ''; ca = '' dir = ${x509.dir}/pub private_key = $dir/sec/key.pem crl_dir = $dir crlnumber = $dir/crl.num crl = $dir/crl.pem database = $dir/idx.txt ''; }; }; opensslConf_user-cert = lib.mkOption { type = (with types; attrsOf string); default = { user_cert_extensions = '' basicConstraints = critical,CA:FALSE,pathlen:0 keyUsage = digitalSignature,keyEncipherment subjectAltName = email:$ENV::user@${x509.host} subjectKeyIdentifier = hash issuerAltName = issuer:copy authorityKeyIdentifier = keyid:always,issuer:always authorityInfoAccess = caIssuers;URI:http://${x509.distributionHost}/x509/${x509.host}.cert.pem ''; }; }; }; config = { systemd.services.x509 = { enable = true; wantedBy = [ "multi-user.target" ]; before = [ "keys.target" ]; serviceConfig = { ExecStart = pkgs.writeShellScriptBin "x509.service" ( lib.optionalString (x509.scheme == "self-signed") '' # Generate DH parameters { ${pkgs.openssl}/bin/openssl dhparam -in ${x509.dir}/dh.pem -text | head -n 1 | ${pkgs.gnugrep}/bin/grep >/dev/null "(${toString x509.keySize} bit)" } || { mkdir -p ${x509.dir} ${pkgs.openssl}/bin/openssl dhparam \ ${toString x509.dh} \ >${x509.dir}/dh.pem } # Make private key [ -s "${x509.key}" ] || { mkdir -p ${x509.dir} ( umask 077 "${pkgs.openssl}/bin/openssl" genrsa \ -out "${x509.key}" \ -rand /dev/urandom \ ${toString x509.keySize} ) } # Make self-signed certificate [ -s "${x509.cert}" ] && [ "${x509.cert}" -nt "${x509.key}" ] || { user= \ ${pkgs.openssl}/bin/openssl req \ -batch \ -new \ -x509 \ -utf8 \ -rand /dev/urandom \ -config ${x509.opensslConf} \ -extensions self_signed_extensions \ -reqexts self_signed_extensions \ -inform PEM \ -outform PEM \ -keyform PEM \ ${when x509.days "-days ${toString x509.days}"} \ -set_serial 0x$(sleep 1; date '+%Y%m%d%H%M%S') \ -reqopt no_pubkey,no_sigdump \ -key ${x509.key} \ -out ${x509.cert} } '') + "/bin/x509.service"; }; }; }; }