diff --git a/nixos/modules/issuer.nix b/nixos/modules/issuer.nix index fdf63d644e61549c8d5791b6584cd3ac32aa6687..2222d834c3aab43638ec9c7333bac7dfe9b4c90a 100644 --- a/nixos/modules/issuer.nix +++ b/nixos/modules/issuer.nix @@ -56,6 +56,27 @@ in { and payment management. ''; }; + services.private-storage-issuer.stripeEndpointDomain = lib.mkOption { + type = lib.types.str; + description = '' + The domain name for the Stripe API HTTP endpoint. + ''; + default = "api.stripe.com"; + }; + services.private-storage-issuer.stripeEndpointScheme = lib.mkOption { + type = lib.types.enum [ "HTTP" "HTTPS" ]; + description = '' + Whether to use HTTP or HTTPS for the Stripe API. + ''; + default = "HTTPS"; + }; + services.private-storage-issuer.stripeEndpointPort = lib.mkOption { + type = lib.types.int; + description = '' + The port number for the Stripe API HTTP endpoint. + ''; + default = 443; + }; services.private-storage-issuer.database = lib.mkOption { default = "Memory"; type = lib.types.enum [ "Memory" "SQLite3" ]; @@ -144,7 +165,11 @@ in { originStrings = map prefixOption cfg.allowedChargeOrigins; originArgs = builtins.concatStringsSep " " originStrings; - stripeArgs = "--stripe-key-path ${cfg.stripeSecretKeyPath}"; + stripeArgs = + "--stripe-key-path ${cfg.stripeSecretKeyPath} " + + "--stripe-endpoint-domain ${cfg.stripeEndpointDomain} " + + "--stripe-endpoint-scheme ${cfg.stripeEndpointScheme} " + + "--stripe-endpoint-port ${toString cfg.stripeEndpointPort}"; in "${cfg.package}/bin/PaymentServer-exe ${originArgs} ${issuerArgs} ${databaseArgs} ${httpsArgs} ${stripeArgs}"; }; diff --git a/nixos/modules/tests/get-passes.py b/nixos/modules/tests/get-passes.py index d76ce3ccbf703bfabd08e120f84ed39063339297..96875713233b7c46abae0cfc3f0d946628bf8cc2 100755 --- a/nixos/modules/tests/get-passes.py +++ b/nixos/modules/tests/get-passes.py @@ -12,23 +12,25 @@ from json import dumps from time import sleep def main(): - clientAPIRoot, issuerAPIRoot = argv[1:] + if len(argv) != 4: + raise SystemExit( + "usage: %s <client api root> <issuer api root> <voucher>", + ) + clientAPIRoot, issuerAPIRoot, voucher = 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" - # Simulate a payment for a voucher. - post( - issuerAPIRoot + "v1/stripe/webhook", - dumps(charge_succeeded_json(voucher)), + # Submit a charge to the issuer (which is also the PaymentServer). + charge_response = post( + issuerAPIRoot + "v1/stripe/charge", + dumps(charge_json(voucher)), headers={"content-type": "application/json"}, ) + charge_response.raise_for_status() # Tell the client to redeem the voucher. response = put( @@ -64,141 +66,13 @@ def retry(description, f): raise ValueError("failed to {} after many tries".format(description)) -def charge_succeeded_json(voucher): - # This structure copy/pasted from Stripe webhook web interface. - base_payload = { - "id": "evt_1FKSX2DeTd13VRuuhPaUDA2f", - "object": "event", - "api_version": "2016-07-06", - "created": 1568910660, - "data": { - "object": { - "id": "ch_1FKSX2DeTd13VRuuG9BXbqji", - "object": "charge", - "amount": 999, - "amount_refunded": 0, - "application": None, - "application_fee": None, - "application_fee_amount": None, - "balance_transaction": "txn_1FKSX2DeTd13VRuuqO1CJZ1e", - "billing_details": { - "address": { - "city": None, - "country": None, - "line1": None, - "line2": None, - "postal_code": None, - "state": None - }, - "email": None, - "name": None, - "phone": None - }, - "captured": True, - "created": 1568910660, - "currency": "usd", - "customer": None, - "description": None, - "destination": None, - "dispute": None, - "failure_code": None, - "failure_message": None, - "fraud_details": { - }, - "invoice": None, - "livemode": False, - "metadata": { - "Voucher": None, - }, - "on_behalf_of": None, - "order": None, - "outcome": { - "network_status": "approved_by_network", - "reason": None, - "risk_level": "normal", - "risk_score": 44, - "seller_message": "Payment complete.", - "type": "authorized" - }, - "paid": True, - "payment_intent": None, - "payment_method": "card_1FKSX2DeTd13VRuus5VEjmjG", - "payment_method_details": { - "card": { - "brand": "visa", - "checks": { - "address_line1_check": None, - "address_postal_code_check": None, - "cvc_check": None - }, - "country": "US", - "exp_month": 9, - "exp_year": 2020, - "fingerprint": "HTJeRR4MXhAAkctF", - "funding": "credit", - "last4": "4242", - "three_d_secure": None, - "wallet": None - }, - "type": "card" - }, - "receipt_email": None, - "receipt_number": None, - "receipt_url": "https://pay.stripe.com/receipts/acct_198xN4DeTd13VRuu/ch_1FKSX2DeTd13VRuuG9BXbqji/rcpt_Fq8oAItSiNmcm0beiie6lUYin920E7a", - "refunded": False, - "refunds": { - "object": "list", - "data": [ - ], - "has_more": False, - "total_count": 0, - "url": "/v1/charges/ch_1FKSX2DeTd13VRuuG9BXbqji/refunds" - }, - "review": None, - "shipping": None, - "source": { - "id": "card_1FKSX2DeTd13VRuus5VEjmjG", - "object": "card", - "address_city": None, - "address_country": None, - "address_line1": None, - "address_line1_check": None, - "address_line2": None, - "address_state": None, - "address_zip": None, - "address_zip_check": None, - "brand": "Visa", - "country": "US", - "customer": None, - "cvc_check": None, - "dynamic_last4": None, - "exp_month": 9, - "exp_year": 2020, - "fingerprint": "HTJeRR4MXhAAkctF", - "funding": "credit", - "last4": "4242", - "metadata": { - }, - "name": None, - "tokenization_method": None - }, - "source_transfer": None, - "statement_descriptor": None, - "statement_descriptor_suffix": None, - "status": "succeeded", - "transfer_data": None, - "transfer_group": None - } - }, - "livemode": False, - "pending_webhooks": 2, - "request": "req_5ozjOIAcOkvVUK", - "type": "charge.succeeded" +def charge_json(voucher): + return { + "token": "tok_abcdef", + "voucher": voucher, + "amount": "100", + "currency": "USD", } - # Indicate the voucher the payment references. - base_payload["data"]["object"]["metadata"]["Voucher"] = voucher - return base_payload - if __name__ == '__main__': main() diff --git a/nixos/modules/tests/private-storage.nix b/nixos/modules/tests/private-storage.nix index 9028691fc8edb133b5c048291cd5f0741f6f4ea1..1f6d40cb30ebe9f953c882d428746f2593190d36 100644 --- a/nixos/modules/tests/private-storage.nix +++ b/nixos/modules/tests/private-storage.nix @@ -9,9 +9,16 @@ let get-passes = ./get-passes.py; exercise-storage = ./exercise-storage.py; + # This is a test double of the Stripe API server. It is extremely simple. + # It barely knows how to respond to exactly the API endpoints we use, + # exactly how we use them. + stripe-api-double = ./stripe-api-double.py; + # The root URL of the Ristretto-flavored PrivacyPass issuer API. issuerURL = "http://issuer/"; + voucher = "xyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxy"; + # 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 # world at large. @@ -42,6 +49,22 @@ let networking.firewall.enable = false; networking.dhcpcd.enable = false; }; + + # Return a Perl program fragment to run a shell command on one of the nodes. + # The first argument is the name of the node. The second is a list of the + # argv to run. + # + # The program's output is piped to systemd-cat and the Perl fragment + # evaluates to success if the command exits with a success status. + runOnNode = node: argv: + let + command = builtins.concatStringsSep " " argv; + in + " + \$${node}->succeed('set -eo pipefail; ${command} | systemd-cat'); + # succeed() is not success but 1 is. + 1; + "; in # https://nixos.org/nixos/manual/index.html#sec-nixos-tests import <nixpkgs/nixos/tests/make-test.nix> { @@ -69,11 +92,13 @@ import <nixpkgs/nixos/tests/make-test.nix> { { imports = [ ../private-storage.nix ]; - services.private-storage.enable = true; - services.private-storage.publicIPv4 = "storage"; - services.private-storage.introducerFURL = introducerFURL; - services.private-storage.issuerRootURL = issuerURL; - services.private-storage.ristrettoSigningKeyPath = pkgs.writeText "signing-key.private" ristrettoSigningKey; + services.private-storage = { + enable = true; + publicIPv4 = "storage"; + introducerFURL = introducerFURL; + issuerRootURL = issuerURL; + inherit ristrettoSigningKeyPath; + }; } // networkConfig; # Operate an issuer as well. @@ -90,8 +115,28 @@ import <nixpkgs/nixos/tests/make-test.nix> { inherit ristrettoSigningKey; stripeSecretKeyPath = pkgs.writeText "stripe.secret" stripeSecretKey; letsEncryptAdminEmail = "user@example.invalid"; + stripeEndpointDomain = "api_stripe_com"; + stripeEndpointScheme = "HTTP"; + stripeEndpointPort = 80; }; } // networkConfig; + + "api_stripe_com" = + { config, pkgs, ... }: + let python = pkgs.python3.withPackages (ps: [ ps.twisted ]); + in networkConfig // { + environment.systemPackages = [ + python + pkgs.curl + ]; + + systemd.services."api.stripe.com" = { + enable = true; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + script = "${python}/bin/python ${stripe-api-double} tcp:80"; + }; + }; }; # Test the machines with a Perl program (sobbing). @@ -107,13 +152,7 @@ import <nixpkgs/nixos/tests/make-test.nix> { ); eval { - $introducer->succeed( - 'set -eo pipefail; ' . - '${run-introducer} /tmp/node.pem ${toString introducerPort} ${introducerFURL} | ' . - 'systemd-cat' - ); - # Signal success. :/ - 1; + ${runOnNode "introducer" [ run-introducer "/tmp/node.pem" (toString introducerPort) introducerFURL ]} } or do { my $error = $@ || 'Unknown failure'; my ($code, $log) = $introducer->execute('cat /tmp/stdout /tmp/stderr'); @@ -142,26 +181,32 @@ import <nixpkgs/nixos/tests/make-test.nix> { # # Storage appears to be working so try to get a client to speak with it. # - $client->succeed('set -eo pipefail; ${run-client} ${introducerFURL} ${issuerURL} | systemd-cat'); + ${runOnNode "client" [ run-client introducerFURL issuerURL ]} $client->waitForOpenPort(3456); # Get some ZKAPs from the issuer. eval { - $client->succeed('set -eo pipefail; ${get-passes} http://127.0.0.1:3456 ${issuerURL} | systemd-cat'); - # succeed() is not success but 1 is. + $api_stripe_com->waitForUnit("api.stripe.com"); 1; + } or do { + my ($code, $log) = $api_stripe_com->execute('journalctl -u api.stripe.com'); + $api_stripe_com->log($log); + die $@; + }; + eval { + ${runOnNode "client" [ get-passes "http://127.0.0.1:3456" issuerURL voucher ]} } or do { my $error = $@ || 'Unknown failure'; my ($code, $log) = $client->execute('cat /tmp/stdout /tmp/stderr'); $client->log($log); + my ($code, $log) = $api_stripe_com->execute('journalctl -u api.stripe.com'); + $api_stripe_com->log($log); die $@; }; # The client should be prepped now. Make it try to use some storage. eval { - $client->succeed('set -eo pipefail; ${exercise-storage} /tmp/client | systemd-cat'); - # nothing succeeds like ... 1. - 1; + ${runOnNode "client" [ exercise-storage "/tmp/client" ]} } or do { my $error = $@ || 'Unknown failure'; my ($code, $log) = $client->execute('cat /tmp/stdout /tmp/stderr'); diff --git a/nixos/modules/tests/stripe-api-double.py b/nixos/modules/tests/stripe-api-double.py new file mode 100755 index 0000000000000000000000000000000000000000..5b96c0befe2a9434560bdd357f3d8e8dc2a92bc5 --- /dev/null +++ b/nixos/modules/tests/stripe-api-double.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 + +from sys import stdout, argv +from json import dumps + +from twisted.internet.defer import Deferred +from twisted.internet.endpoints import serverFromString +from twisted.internet.task import react +from twisted.web.resource import Resource +from twisted.web.server import Site +from twisted.python.log import startLogging + +class Charges(Resource): + def render_POST(self, request): + voucher = request.args[b"metadata[Voucher]"][0].decode("utf-8") + card = request.args[b"card"][0].decode("utf-8") + amount = int(request.args[b"amount"][0]) + currency = request.args[b"currency"][0].decode("utf-8") + response = dumps(charge(card, amount, currency, {u"Voucher": voucher})) + return response.encode("utf-8") + +def main(reactor, listenEndpoint): + charges = Charges() + v1 = Resource() + v1.putChild(b"charges", charges) + root = Resource() + root.putChild(b"v1", v1) + + return serverFromString(reactor, listenEndpoint).listen( + Site(root), + ).addCallback( + lambda ignored: Deferred() + ) + +def charge(source, amount, currency, metadata): + return { + "id": "ch_1Fj8frBHXBAMm9bPkekylvAq", + "object": "charge", + "amount": amount, + "amount_refunded": 0, + "application": None, + "application_fee": None, + "application_fee_amount": None, + "balance_transaction": "txn_1Fj8fr2eZvKYlo2CC5JzIGj5", + "billing_details": { + "address": { + "city": None, + "country": None, + "line1": None, + "line2": None, + "postal_code": None, + "state": None + }, + "email": None, + "name": None, + "phone": None + }, + "captured": False, + "created": 1574792527, + "currency": currency, + "customer": None, + "description": None, + "dispute": None, + "disputed": False, + "failure_code": None, + "failure_message": None, + "fraud_details": {}, + "invoice": None, + "livemode": False, + "metadata": metadata, + "on_behalf_of": None, + "order": None, + "outcome": None, + "paid": True, + "payment_intent": None, + "payment_method": source, + "payment_method_details": {}, + "receipt_email": None, + "receipt_number": None, + "receipt_url": "https://pay.stripe.com/receipts/acct_1FhhxTBHXBAMm9bP/ch_1Fj8frBHXBAMm9bPkekylvAq/rcpt_GFdxYuDoGKfYgokh9YA11XhnYC7Gnxp", + "refunded": False, + "refunds": { + "object": "list", + "data": [], + "has_more": False, + "url": "/v1/charges/ch_1Fj8frBHXBAMm9bPkekylvAq/refunds" + }, + "review": None, + "shipping": None, + "source_transfer": None, + "statement_descriptor": None, + "statement_descriptor_suffix": None, + "status": "succeeded", + "transfer_data": None, + "transfer_group": None, + "source": source, + } + +if __name__ == '__main__': + startLogging(stdout) + react(main, argv[1:]) diff --git a/nixos/pkgs/zkapissuer-repo.nix b/nixos/pkgs/zkapissuer-repo.nix index d31233b41997a94b424a1eacf47d92423768c3d6..a252d3a9b31cc83ef879e1a1f9561f0480860d1f 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 = "e1e0817b80ce1031d21587357bfb8dc4e7837b01"; - sha256 = "0i95cpqbz9fj4b1cii1xh01ss77v2jgd5qwzqvk812f0slz3fw6q"; + rev = "1130b17e85392efd9f6be733308542b50bded1e3"; + sha256 = "1ivcy3xcakxs0yfvbnvizq9pchp15g2wdprh5r5rq4fkqk8k6nbf"; } \ No newline at end of file