From 19eedc0be2419caf14cbaf1dbe3fc4bffb120431 Mon Sep 17 00:00:00 2001
From: Jean-Paul Calderone <exarkun@twistedmatrix.com>
Date: Wed, 30 Oct 2019 15:13:39 -0400
Subject: [PATCH] enable tls with acme certificates

---
 nixos/modules/issuer.nix                | 82 ++++++++++++++++++++++++-
 nixos/modules/tests/get-passes.py       |  8 ++-
 nixos/modules/tests/private-storage.nix |  6 +-
 nixos/pkgs/zkapissuer-repo.nix          |  4 +-
 4 files changed, 91 insertions(+), 9 deletions(-)

diff --git a/nixos/modules/issuer.nix b/nixos/modules/issuer.nix
index 41143ce0..2c1fab77 100644
--- a/nixos/modules/issuer.nix
+++ b/nixos/modules/issuer.nix
@@ -15,6 +15,23 @@ in {
         The package to use for the ZKAP issuer.
       '';
     };
+    services.private-storage-issuer.domain = lib.mkOption {
+      default = "payments.privatestorage.io";
+      type = lib.types.str;
+      example = lib.literalExample "payments.example.com";
+      description = ''
+        The domain name at which the issuer is reachable.
+      '';
+    };
+    services.private-storage-issuer.tls = lib.mkOption {
+      default = true;
+      type = lib.types.bool;
+      description = ''
+        Whether or not to listen on TLS.  For real-world use you should always
+        listen on TLS.  This is provided as an aid to automated testing where
+        it might be difficult to obtain a real certificate.
+      '';
+    };
     services.private-storage-issuer.issuer = lib.mkOption {
       default = "Ristretto";
       type = lib.types.enum [ " Trivial" "Ristretto" ];
@@ -49,13 +66,22 @@ in {
     };
   };
 
-  config = lib.mkIf cfg.enable {
+  config =
+    let
+      acme = "/var/lib/acme";
+    in lib.mkIf cfg.enable {
     # Add a systemd service to run PaymentServer.
     systemd.services.zkapissuer = {
       enable = true;
       description = "ZKAP Issuer";
       wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" ];
+      after = [
+        # Make sure there is a network so we can bind to all of the
+        # interfaces.
+        "network.target"
+      ];
+      # Make sure we at least have a self-signed certificate.
+      requires = lib.optional cfg.tls "acme-selfsigned-${cfg.domain}.service";
 
       serviceConfig = {
         ExecStart =
@@ -70,8 +96,21 @@ in {
               if cfg.database == "Memory"
                 then "--database Memory"
                 else "--database SQLite3 --database-path ${cfg.databasePath}";
+            httpsArgs =
+              if cfg.tls
+              then
+                "--https-port 443 " +
+                # acme has plugins to write the files in different ways but the
+                # self-signed certificate generator doesn't.  The files it
+                # writes are weirdly named and shaped but they work.
+                "--https-certificate-path ${acme}/${cfg.domain}/full.pem " +
+                "--https-certificate-chain-path ${acme}/${cfg.domain}/fullchain.pem " +
+                "--https-key-path ${acme}/${cfg.domain}/key.pem"
+              else
+                # Only for automated testing.
+                "--http-port 80";
           in
-            "${cfg.package}/bin/PaymentServer-exe ${issuerArgs} ${databaseArgs}";
+            "${cfg.package}/bin/PaymentServer-exe ${issuerArgs} ${databaseArgs} ${httpsArgs}";
         Type = "simple";
         # It really shouldn't ever exit on its own!  If it does, it's a bug
         # we'll have to fix.  Restart it and hope it doesn't happen too much
@@ -79,5 +118,42 @@ in {
         Restart = "always";
       };
     };
+
+    # Certificate renewal.  Note that preliminarySelfsigned only creates the
+    # service.  We must declare that we *require* it in our service above.
+    security.acme = if cfg.tls
+      then {
+        production = false;
+        preliminarySelfsigned = true;
+        certs."${cfg.domain}" = {
+          email = "jean-paul@privatestorage.io";
+          postRun = "systemctl restart zkapissuer.service";
+          webroot = "${acme}/acme-challenges";
+          plugins = [ "full.pem" "fullchain.pem" "key.pem" ];
+        };
+      }
+      else {};
+
+    systemd.timers = if cfg.tls
+      then {
+        "acme-${cfg.domain}-initial" = config.systemd.timers."acme-${cfg.domain}" // {
+          timerConfig = {
+            OnUnitActiveSec = "0";
+            Unit = "acme-${cfg.domain}.service";
+            Persistent = "yes";
+            AccuracySec = "1us";
+            RandomizedDelaySec = "0s";
+          };
+        };
+      }
+      else {};
+
+    services.nginx.virtualHosts = if cfg.tls
+      then {
+        "${cfg.domain}" = {
+          locations."/" = "${acme}/acme-challenges";
+        };
+      }
+      else {};
   };
 }
diff --git a/nixos/modules/tests/get-passes.py b/nixos/modules/tests/get-passes.py
index ac5cf790..d76ce3cc 100755
--- a/nixos/modules/tests/get-passes.py
+++ b/nixos/modules/tests/get-passes.py
@@ -13,15 +13,19 @@ from time import sleep
 
 def main():
     clientAPIRoot, issuerAPIRoot = argv[1:]
+    if not clientAPIRoot.endswith("/"):
+        clientAPIRoot += "/"
+    if not issuerAPIRoot.endswith("/"):
+        issuerAPIRoot += "/"
 
     # Construct a voucher that's acceptable to various parts of the system.
     voucher = "a" * 44
 
-    zkapauthz = clientAPIRoot + "/storage-plugins/privatestorageio-zkapauthz-v1"
+    zkapauthz = clientAPIRoot + "storage-plugins/privatestorageio-zkapauthz-v1"
 
     # Simulate a payment for a voucher.
     post(
-        issuerAPIRoot + "/v1/stripe/webhook",
+        issuerAPIRoot + "v1/stripe/webhook",
         dumps(charge_succeeded_json(voucher)),
         headers={"content-type": "application/json"},
     )
diff --git a/nixos/modules/tests/private-storage.nix b/nixos/modules/tests/private-storage.nix
index cc2fa798..b731404d 100644
--- a/nixos/modules/tests/private-storage.nix
+++ b/nixos/modules/tests/private-storage.nix
@@ -10,7 +10,7 @@ let
   exercise-storage = ./exercise-storage.py;
 
   # The root URL of the Ristretto-flavored PrivacyPass issuer API.
-  issuerURL = "http://issuer:8081/";
+  issuerURL = "http://issuer/";
 
   # The issuer's signing key.  Notionally, this is a secret key.  This is only
   # the value for this system test though so I don't care if it leaks to the
@@ -81,6 +81,8 @@ import <nixpkgs/nixos/tests/make-test.nix> {
       ];
       services.private-storage-issuer = {
         enable = true;
+        domain = "issuer";
+        tls = false;
         issuer = "Ristretto";
         inherit ristrettoSigningKey;
       };
@@ -140,7 +142,7 @@ import <nixpkgs/nixos/tests/make-test.nix> {
 
       # Get some ZKAPs from the issuer.
       eval {
-        $client->succeed('set -eo pipefail; ${get-passes} http://127.0.0.1:3456 http://issuer:8081 | systemd-cat');
+        $client->succeed('set -eo pipefail; ${get-passes} http://127.0.0.1:3456 ${issuerURL} | systemd-cat');
         # succeed() is not success but 1 is.
         1;
       } or do {
diff --git a/nixos/pkgs/zkapissuer-repo.nix b/nixos/pkgs/zkapissuer-repo.nix
index 44182649..06faac68 100644
--- a/nixos/pkgs/zkapissuer-repo.nix
+++ b/nixos/pkgs/zkapissuer-repo.nix
@@ -4,6 +4,6 @@ in
   pkgs.fetchFromGitHub {
     owner = "PrivateStorageio";
     repo = "PaymentServer";
-    rev = "028d26152eba4f034aba405caa17627a764c2bbe";
-    sha256 = "06hdln97r2ign7phf661wlzh3z06bk9906lvc0gm3lh1pa23d3gb";
+    rev = "0acf43493a7525b0eba01b1c69fb69b41f411ecc";
+    sha256 = "0zaqm6qmylclxn25vghafwqxpm5h13c727x3zjnqgp7vbmi2aglk";
   }
\ No newline at end of file
-- 
GitLab