diff --git a/.circleci/config.yml b/.circleci/config.yml
index 691a231518ffe4c9e7aed7b3f75ee862d8de8170..8c947f76e0e2a30e4b4267eb26e903c8171644dd 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -24,46 +24,16 @@ jobs:
 
     resource_class: "xlarge"
 
-    environment:
-      # Specify a revision of NixOS/nixpkgs to run against.  This essentially
-      # pins the majority of the software involved in the build.  This
-      # revision is selected arbitrarily.  It's somewhat current as of the
-      # time of this comment.  We can bump it to a newer version when that
-      # makes sense.  Meanwhile, the platform won't shift around beneath us
-      # unexpectedly.
-      NIXPKGS_REV: "92609f3d9bc3acffbdbe54fa1c591a885612aa73"
-
     steps:
       - run:
           name: "Setup Environment Variables"
           command: |
-            # Get NIX_PATH set for the rest of the job so that the revision of
-            # nixpkgs we selected will be used everywhere Nix pulls in software.
-            # There is no way to set an environment variable containing the
-            # value of another environment variable on CircleCI except to use
-            # the `BASH_ENV` feature as we do here.
-            echo "export NIX_PATH=nixpkgs=https://github.com/NixOS/nixpkgs/archive/$NIXPKGS_REV.tar.gz" >> $BASH_ENV
             # Set XDG_CONFIG_DIRS to point at the source directory, so that nix
             # will pickup nix/nix.conf as a configuration file from there.
             echo "export XDG_CONFIG_DIRS=$CIRCLE_WORKING_DIRECTORY" >> $BASH_ENV
 
       # Get *our* source code.
       - "checkout"
-
-      - "run":
-          # CircleCI won't let us interpolate NIXPKGS_REV into a cache key.
-          # Only CircleCI's own environment variables or variables set via the
-          # web interface in a "context" can be interpolated into cache keys.
-          # However, we can interpolate the checksum of a file...  Since we
-          # don't care about the exact revision, we just care that a new
-          # revision gives us a new string, we can write the revision to a
-          # file and then put the checksum of that file into the cache key.
-          # This way, we don't have to maintain the nixpkgs revision in two
-          # places and risk having them desynchronize.
-          name: "Prepare For Cache Key"
-          command: |
-            echo "${NIXPKGS_REV}" > nixpkgs.rev
-
       - restore_cache:
           # Get all of Nix's state relating to the particular revision of
           # nixpkgs we're using.  It will always be the same.  CircleCI
@@ -87,9 +57,8 @@ jobs:
             # If nixpkgs changes then potentially a lot of cached packages for
             # the base system will be invalidated so we may as well drop them
             # and make a new cache with the new packages.
-            - paymentserver-nix-store-v5-{{ checksum "nixpkgs.rev" }}-{{ checksum "nix/challenge-bypass-ristretto-repo.nix" }}
-            - paymentserver-nix-store-v5-{{ checksum "nixpkgs.rev" }}-
-            - paymentserver-nix-store-v5-
+            - paymentserver-nix-store-v6-{{ checksum "nix/sources.json" }}
+            - paymentserver-nix-store-v6-
 
       - run:
           name: "Building with Nix"
@@ -114,7 +83,7 @@ jobs:
 
       - save_cache:
           name: "Cache Nix Store Paths"
-          key: paymentserver-nix-store-v5-{{ checksum "nixpkgs.rev" }}-{{ checksum "nix/challenge-bypass-ristretto-repo.nix" }}
+          key: paymentserver-nix-store-v6-{{ checksum "nix/sources.json" }}
           paths:
             - "/nix"
 
diff --git a/PaymentServer.cabal b/PaymentServer.cabal
index 1a96e000a5642345389b961adb1288a8b3d2df6e..1b29cf00f39ed99b0734f253d67636a6d43315e3 100644
--- a/PaymentServer.cabal
+++ b/PaymentServer.cabal
@@ -67,6 +67,15 @@ executable PaymentServer-generate-key
                      , PaymentServer
   default-language:    Haskell2010
 
+executable PaymentServer-get-public-key
+  hs-source-dirs:      get-public-key
+  main-is:             Main.hs
+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N -Wmissing-import-lists -Wunused-imports
+  build-depends:       base
+                     , text
+                     , PaymentServer
+  default-language:    Haskell2010
+
 test-suite PaymentServer-tests
   type:                exitcode-stdio-1.0
   hs-source-dirs:      test
diff --git a/get-public-key/Main.hs b/get-public-key/Main.hs
new file mode 100644
index 0000000000000000000000000000000000000000..952caef4c26f5eaa58d51ca3e222fe2d2a1c5409
--- /dev/null
+++ b/get-public-key/Main.hs
@@ -0,0 +1,21 @@
+-- | Extract a public key from Ristretto-flavored PrivacyPass signing key read from stdin.
+module Main
+  ( main
+  ) where
+
+import Prelude hiding
+  ( putStrLn
+  , getLine
+  )
+
+import Data.Text.IO
+  ( putStrLn
+  , getLine
+  )
+
+import PaymentServer.Ristretto
+  ( getPublicKey
+  )
+
+main :: IO ()
+main = getLine >>= getPublicKey >>= putStrLn
diff --git a/nix/challenge-bypass-ristretto-repo.nix b/nix/challenge-bypass-ristretto-repo.nix
deleted file mode 100644
index 1357efe3e9ab3c9480109c7a52864f751d9bfdc3..0000000000000000000000000000000000000000
--- a/nix/challenge-bypass-ristretto-repo.nix
+++ /dev/null
@@ -1,9 +0,0 @@
-let
-  pkgs = import <nixpkgs> {};
-in
-  pkgs.fetchFromGitHub {
-    owner = "LeastAuthority";
-    repo = "python-challenge-bypass-ristretto";
-    rev = "v2021.07.12";
-    sha256 = "16af1qmx7srhvcc936x7hl2bz50hafm39311dbzqam9ms1i5q89j";
-  }
diff --git a/nix/challenge-bypass-ristretto.nix b/nix/challenge-bypass-ristretto.nix
index dc08608de18a4fec80c90e75839b52c11817d59f..10bf5e88a11dff116c80776a91f53c8561d166aa 100644
--- a/nix/challenge-bypass-ristretto.nix
+++ b/nix/challenge-bypass-ristretto.nix
@@ -1,6 +1,8 @@
 # Provide the ffi bindings to the Rust challenge-bypass-ristretto library.
-{ fetchFromGitHub, callPackage }:
 let
-  src = import ./challenge-bypass-ristretto-repo.nix;
+  sources = import ./sources.nix;
 in
-  import "${src}/default-challenge-bypass-ristretto-ffi.nix" { }
+{ callPackage
+, libchallenge_bypass_ristretto_ffi_repo ? sources.libchallenge_bypass_ristretto_ffi
+}:
+  callPackage "${libchallenge_bypass_ristretto_ffi_repo}/challenge-bypass-ristretto.nix" { }
diff --git a/nix/materialized.paymentserver/PaymentServer.nix b/nix/materialized.paymentserver/PaymentServer.nix
index e77024c3f3ce41bf84ff5a0390d25744852739ce..842157cc790e792b94c19bb6ca1ee4291bd5b749 100644
--- a/nix/materialized.paymentserver/PaymentServer.nix
+++ b/nix/materialized.paymentserver/PaymentServer.nix
@@ -93,6 +93,16 @@
           hsSourceDirs = [ "generate-key" ];
           mainPath = [ "Main.hs" ];
           };
+        "PaymentServer-get-public-key" = {
+          depends = [
+            (hsPkgs."base" or (errorHandler.buildDepError "base"))
+            (hsPkgs."text" or (errorHandler.buildDepError "text"))
+            (hsPkgs."PaymentServer" or (errorHandler.buildDepError "PaymentServer"))
+            ];
+          buildable = true;
+          hsSourceDirs = [ "get-public-key" ];
+          mainPath = [ "Main.hs" ];
+          };
         };
       tests = {
         "PaymentServer-tests" = {
diff --git a/nix/sources.json b/nix/sources.json
index a6a9c21c29e2a869d2a6b820fde41ee535314819..a4fb79ffaf80d6c31966860200d263ff20e7a362 100644
--- a/nix/sources.json
+++ b/nix/sources.json
@@ -11,6 +11,18 @@
         "url": "https://github.com/input-output-hk/haskell.nix/archive/e95a1f0dacbc64603c31d11e36e4ba1af8f0eb43.tar.gz",
         "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
     },
+    "libchallenge_bypass_ristretto_ffi": {
+        "branch": "master",
+        "description": "Python bindings for Brave's challenge-bypass-ristretto library",
+        "homepage": null,
+        "owner": "leastauthority",
+        "repo": "python-challenge-bypass-ristretto",
+        "rev": "6e25af8a721a9d8507745efad3d15dae75b78909",
+        "sha256": "0w5vpq9kqhdbbynbbky1ibx1im5582yacqnb9y2y3h114diywdyq",
+        "type": "tarball",
+        "url": "https://github.com/leastauthority/python-challenge-bypass-ristretto/archive/6e25af8a721a9d8507745efad3d15dae75b78909.tar.gz",
+        "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
+    },
     "niv": {
         "branch": "master",
         "description": "Easy dependency management for Nix projects",
diff --git a/src/PaymentServer/Ristretto.hs b/src/PaymentServer/Ristretto.hs
index 7c065206f5d93963bd410fbd588c17c41e726942..1018eb5cdc6b17236ce0d6ce1ee76906ca2cc206 100644
--- a/src/PaymentServer/Ristretto.hs
+++ b/src/PaymentServer/Ristretto.hs
@@ -4,6 +4,7 @@
 module PaymentServer.Ristretto
   ( Issuance(Issuance)
   , randomSigningKey
+  , getPublicKey
   , ristretto
   ) where
 
@@ -159,3 +160,14 @@ randomSigningKey = do
   result <- peekCString cString
   free cString
   return $ pack result
+
+-- | getPublicKey returns the base64 encoded public key corresponding to the
+-- base64 encoded signing key passed to it.
+getPublicKey :: Text -> IO Text
+getPublicKey enc_skey = do
+  enc_cstr_skey <- newCString . unpack $ enc_skey
+  skey <- signing_key_decode_base64 $ enc_cstr_skey
+  pkey <- signing_key_get_public_key skey
+  enc_cstr_pkey <- public_key_encode_base64 pkey
+  enc_pkey <- peekCString enc_cstr_pkey
+  return $ pack enc_pkey