diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py index 844a06ed8ec681514de688be710fc08c0a801978..f415824d732f77c09b4afd7fc26d2b348053adb4 100644 --- a/src/_zkapauthorizer/controller.py +++ b/src/_zkapauthorizer/controller.py @@ -222,6 +222,11 @@ class RistrettoRedeemer(object): def tokens_to_passes(self, message, unblinded_tokens): # XXX Here's some more of the privacypass dance. Something needs to # know to call this, I guess? Also it's untested as heck. + unblinded_tokens = list( + privacypass.UnblindedToken.decode_base64(token.text.encode("ascii")) + for token + in unblinded_tokens + ) clients_preimages = list( token.preimage() for token diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py index f30eeafe409232efa7da74433b49466e4870a3b0..e7e7f92e89003f11c0dcb02f74aaa2660d4920ee 100644 --- a/src/_zkapauthorizer/tests/test_controller.py +++ b/src/_zkapauthorizer/tests/test_controller.py @@ -20,6 +20,9 @@ from json import ( loads, dumps, ) +from functools import ( + partial, +) from zope.interface import ( implementer, ) @@ -73,6 +76,8 @@ from privacypass import ( PublicKey, BlindedToken, BatchDLEQProof, + TokenPreimage, + VerificationSignature, random_signing_key, ) @@ -205,7 +210,10 @@ class RistrettoRedeemerTests(TestCase): """ signing_key = random_signing_key() issuer = RistrettoRedemption(signing_key) - # Make it lie about the public key it is using. + + # Make it lie about the public key it is using. This causes the proof + # to be invalid since it proves the signature was made with a + # different key than reported in the response. issuer.public_key = PublicKey.from_signing_key(random_signing_key()) treq = treq_for_loopback_ristretto(issuer) @@ -226,6 +234,75 @@ class RistrettoRedeemerTests(TestCase): ), ) + @given(vouchers().map(Voucher), integers(min_value=1, max_value=100)) + def test_ristretto_pass_construction(self, voucher, num_tokens): + """ + The passes constructed using unblinded tokens and messages pass the + Ristretto verification check. + """ + message = b"hello world" + + signing_key = random_signing_key() + issuer = RistrettoRedemption(signing_key) + treq = treq_for_loopback_ristretto(issuer) + redeemer = RistrettoRedeemer(treq, NOWHERE) + + random_tokens = redeemer.random_tokens_for_voucher(voucher, num_tokens) + d = redeemer.redeem( + voucher, + random_tokens, + ) + def unblinded_tokens_to_passes(unblinded_tokens): + passes = redeemer.tokens_to_passes(message, unblinded_tokens) + return passes + d.addCallback(unblinded_tokens_to_passes) + + self.assertThat( + d, + succeeded( + AfterPreprocessing( + partial(ristretto_verify, signing_key, message), + Equals(True), + ), + ), + ) + + +def ristretto_verify(signing_key, message, marshaled_passes): + servers_passes = list( + ( + TokenPreimage.decode_base64(token_preimage), + VerificationSignature.decode_base64(sig), + ) + for (token_preimage, sig) + in marshaled_passes + ) + servers_unblinded_tokens = list( + signing_key.rederive_unblinded_token(token_preimage) + for (token_preimage, sig) + in servers_passes + ) + servers_verification_sigs = list( + sig + for (token_preimage, sig) + in servers_passes + ) + servers_verification_keys = list( + unblinded_token.derive_verification_key_sha512() + for unblinded_token + in servers_unblinded_tokens + ) + invalid_passes = list( + key.invalid_sha512( + sig, + message, + ) + for (key, sig) + in zip(servers_verification_keys, servers_verification_sigs) + ) + + return not any(invalid_passes) + def treq_for_loopback_ristretto(local_issuer): """