Lab - 9 - Authentication with ZNP

Introduction

The goal of these exercises is to explore the functionalities of Zero Knowledge Proofs in the scope of a mock authentication process.

Setup

This lab requires that you obtain and install noknow python package available at: https://github.com/jpbarraca/noknow-python

It will include both the noknow system, developed by https://github.com/GoodiesHQ and a client and server developed for this lab.

Obtain the repository, install the requirements and install the package, with the following commands:

sudo apt install python3-virtualenv
git clone https://github.com/jpbarraca/noknow-python.git
cd noknow-python
virtualenv venv
bash
source venv/bin/activate
pip install -r requirements
python3 setup.py install
cd examples

Zero Knowledge Proofs

zero-knowledge proof or zero-knowledge protocol is a method by which one party (the prover) can prove to another party (the verifier) that a given statement is true, while avoiding conveying to the verifier any information beyond the mere fact of the statement’s truth. The intuition underlying zero-knowledge proofs is that it is trivial to prove the possession of certain information by simply revealing it; the challenge is to prove this possession without revealing the information, or any aspect of it whatsoever.

In light of the fact that one should be able to generate a proof of some statement only when in possession of certain secret information connected to the statement, the verifier, even after having become convinced of the statement’s truth, should nonetheless remain unable to prove the statement to third parties. 1

An important protocol in the scope of identities is the Schnorr’s identification protocol, which consists of a public-key based challenge-response identification protocol. In the scope of ZKP, the protocol can operate both in interactive and non-interactive modes, making it very versatile.

The Verifier

In our simplified scenario we will use a Flask webserver that acts as a verifier. Therefore, it will validate the identity of provers (users) that wish to access the resources. For practical purposes, we will also support a register endpoint which allows users to register their identity.

This code is available in the server.py file below. It is a simple Flask webserver with the following properties:

  • Supports register and login methods.
  • The register method allows clients to register their keys, and corresponds to an initial provisioning.
  • During the register phase, the client will set a password which can be later used for authentication. While the password is used in this phase, the key is not disclosed to the server.
  • The login method has two actions: sending a token, which acts as a challenge.
  • The client can take the token and transform it using the password. It proves to know the password without disclosing it.
  • The server also signs their tokens to prevent forgery, and the signing key is unique per client
from flask import Flask
from flask import request
from noknow.core import ZK, ZKSignature, ZKParameters, ZKData, ZKProof
import os
import json

app = Flask(__name__)

clients = {}

@app.route("/")
def index():
    return "Howdy"

@app.route("/register", methods=["POST"])
def register():
    clientid = request.json.get('clientid', None)
    sig = request.json.get('sig', None)
    
    if sig is None or clientid is None:
        return "Invalid request"
    
    if clientid in clients:
        return "Client already registered"

    client_signature = ZKSignature.from_json(sig)
    client_zk = ZK(client_signature.params)
    clients[clientid] = {'zk': client_zk, 'sig': client_signature}

    return "Client registered"


@app.route("/login", methods=["POST"])
def login():
    clientid = request.json.get('clientid', None)
    proof = request.json.get('proof', None)


    # First request, send token to client for proof
    if clientid in clients and proof is None:
        
        # Generate a server password and ZK object for the server
        server_password = os.urandom(32)
        # Set up server component, generating a new ZK object
        server_zk = ZK.new(curve_name="secp384r1", hash_alg="sha3_512")
        # store the server zk and password for later use
        clients[clientid]['server_zk'] = server_zk
        clients[clientid]['server_password'] = server_password
        # get the client zk 
        client_zk = clients[clientid]['zk']

        # Create a signed token and send to the client
        token = server_zk.sign(server_password, client_zk.token())
        return token.to_json()

    if clientid in clients and proof is not None:
        # Get the token from the client
        zkproof = ZKData.from_json(proof)
        token = ZKData.from_json(zkproof.data)

        # Get the client zk and server password
        server_zk = clients[clientid]['server_zk']
        server_signature = server_zk.create_signature(clients[clientid]['server_password'])
        client_zk = clients[clientid]['zk']

        # check if the server signature is valid
        if not server_zk.verify(token, server_signature):
            print("Invalid server auth: ")
            return "Authentication failure"
        else:
            # Verify the proof from the client
            # uses the client proof and signature to verify the token
            result = client_zk.verify(zkproof, clients[clientid]['sig'], data=token)
            return "Authentication success" if result else "Authentication failure"
    
    # Invalid request
    return "Invalid request"

if __name__ == "__main__":
    app.run(debug=True)

The Prover

The prover corresponds to the client, and will execute two actions: register a key and then create a proof that asserts the identity. The proof is only valid for a single client, at a single server. The proof doesn’t include the key but the creation of the proof requires knowing the key. The server can verify that the proof is correct even without knowing the key.

The code for this client is present in the client.py directory and below. The client first registers itself into the server, storing its signature in the server. The signature depends on the password and on a private key which is kept.

Then the client will contact the server and request a token. This token can be used to build a proof together with the password, and is sent to the server, which replies with the result.

The client will ask for a password twice, and you can provide a different password to see what happens (it will fail to prove the identity as expected).

from getpass import getpass
import requests
from noknow.core import ZK, ZKSignature, ZKParameters, ZKData, ZKProof
import uuid


print("Registering client")
client_zk = ZK.new(curve_name="secp256k1", hash_alg="sha3_256")
clientid = uuid.uuid4().hex
password = getpass("Enter Password: ")
# Create signature and send to server
signature = client_zk.create_signature(password)
print("Client: signature ", signature.to_json())
r = requests.post("http://localhost:5000/register", json={"clientid": clientid, "sig": signature.to_json()})
print(r.status_code)

input("Press Enter to continue...")

print("Logging in")
password = getpass("Enter Password: ")
print("Getting token...")
r = requests.post("http://localhost:5000/login", json={"clientid": clientid})
print("Token: ", r.json())
token = r.text
proof = client_zk.sign(password, token).to_json()

print("Proof: ", proof)
print("Sending proof...")

r = requests.post("http://localhost:5000/login", json={"clientid": clientid, "proof": proof})
print(r.text)

TASKS:

  • Obtain the code install the dependencies
  • Execute the server in one terminal and the client on another terminal
  • Provide the same, or different passwords
  • Analyze the messages exchanged

References

Previous
Next