├── .gitignore ├── LICENSE ├── README.md ├── cli.py ├── noknow ├── __init__.py ├── core.py ├── data.py └── utils │ ├── __init__.py │ ├── convert.py │ └── crypto.py ├── requirements.txt ├── setup.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .idea 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | [Bb]in 114 | [Ii]nclude 115 | [Ll]ib 116 | [Ll]ib64 117 | [Ll]ocal 118 | [Ss]cripts 119 | pyvenv.cfg 120 | pip-selfcheck.json 121 | 122 | # Spyder project settings 123 | .spyderproject 124 | .spyproject 125 | 126 | # Rope project settings 127 | .ropeproject 128 | 129 | # mkdocs documentation 130 | /site 131 | 132 | # mypy 133 | .mypy_cache/ 134 | .dmypy.json 135 | dmypy.json 136 | 137 | # Pyre type checker 138 | .pyre/ 139 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 GoodiesHQ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

NoKnow

2 |
3 | Zero-Knowledge Proof implementation in pure python 4 |
5 |
6 |
7 | Experimental 8 |
9 |
10 | 11 | Built with ❤︎ by Austin Archer :) 12 | 13 |
14 |
15 | 16 | 17 | 18 | ## Table of Contents 19 | - [Credits](#credits) 20 | - [Purpose](#purpose) 21 | - [How it Works](#how-it-works) 22 | - [API](#api) 23 | - [Install](#install) 24 | - [Example Usage](#example-usage) 25 | 26 | 27 | ## Credits 28 | This is a slightly modified implementation of Schnorr's protocol that utilizes a state seed. The proofs used are rather complex in nature, but I will do my best to explain its functionality, but please refer to the research papers on which this implementation is based as it does a far more complete job with explanation than I. 29 | 30 | [Elliptic Curve Based Zero Knowledge Proofs and Their 31 | Applicability on Resource Constrained Devices](https://arxiv.org/pdf/1107.1626.pdf) by Ioannis Chatzigiannakis, Apostolos Pyrgelis, Paul G. Spirakis, and Yannis C. Stamatiou 32 | 33 | 34 | ## Purpose 35 | Zero-Knowledge Proofs are undoubtedly the future of authentication security within various IT and application development industrires. The ability to verify the veracity of a claim (ex: proving that you know a secret password), without divulging any information about the claim itself (ex: passwords or hashes), allows for servers to guarantee secure AAA operations (authentication, authorization, and accounting) without exposing private information. `NoKnow` is an implementation of a [Non-Interactive Zero-Knowledge Proof](https://en.wikipedia.org/wiki/Non-interactive_zero-knowledge_proof) protocol specifically designed for verifying text-based secrets, which is ideal for passwords or other authentication means. 36 | 37 | 38 | ## How It Works 39 | The fundamental problem on which this protocol is based is the Elliptic Curve Discrete Logarithm Problem. 40 | 41 | 56 | 57 | 58 | 59 | With this principle in mind, knowing a private variable, `n`, is all that is required to produce the proper point. The first thing to do is generate a signature. This signature is produced by multiplying a known value, such as the hashed result of a password, by the elliptic curve's generator point: 60 | 61 | 66 | 67 | 68 | 69 | Now that we have produced this signature, `S`, which can be represented as an `(x, y)` pair, we can publish this signature publicly so that subsequent messages can be proven to have been produced by the same key that produced the signature, while ensuring that the signature itself reveals nothing about the data used to produce it. 70 | 71 | One of my main goals for developing this library was producing a secure and effective method of Zero-Knowledge authentication. Because messages can be verified against a signature, one method of authentication is for the verifier (server) to produce a random message (called a token, `t`), and send it to the user with a request for them to produce a proof with the provided token that can be verified against their public signature. This ensures that a single proof cannot be re-used by a malicious actor in future authentication attempts. Any proof generated will always be valid against a particular signature, but checking the value of the signed data against what the server expects will ensure, with a large enough random token, it is extremely unlikely that there will ever be a request that provides the same random token. Additionally, another method could be to use a JWT with a short expiration, e.g. 10 seconds, whos validity is checked before processing the proof. However, in this example, I will choose a static random token, `"MyRandomToken"`. 72 | 73 | 91 | 92 | 93 | 94 | Ultimately, this comes down to the fact that some of these values cancel out arithmetically during the proof, so they are simply not needed by the prover. First, let's look at some basic principles of point multiplication with elliptic curves: 95 | 96 | 100 | 101 | 102 | 103 | During the validation project, what is ultimately checked is a hash, namely: 104 | 105 | 108 | 109 | 110 | 111 | Since both `t` and `salt` are public pieces of information, what is actually important is the specific point that is generated. What we need to do is prove: 112 | 123 | 124 | 125 | There we go! We have demonstrated that the point R can be demonstrated to be able to be derived from `c` and `M` without knowing the discriminators of `S` (`k`), `M` (`m`), or `R` (`r`). And since knowledge of all of these are required to create the proof, but their values are not transmitted during proving, the zero knowledge proof is complete.=---=9 126 | 127 | ## API 128 | 129 | The `noknow` Python API is meant to be simple and intuitive: 130 | 131 | ### Core Components 132 | 133 | #### noknow.core.ZKParameters: 134 | The parameters used to initialize the Zero-Knowledge crypto system. 135 | 136 | class ZKParameters(NamedTuple): 137 | """ 138 | Parameters used to construct a ZK proof state using an curve and a random salt 139 | """ 140 | alg: str # Hashing algorithm name 141 | curve: str # Standard Elliptic Curve name to use 142 | s: int # Random salt for the state 143 | 144 | #### noknow.core.ZKSignature: 145 | A crytographic, zero-knowledge signature that can be used to verify future messages. 146 | 147 | class ZKSignature(NamedTuple): 148 | """ 149 | Cryptographic public signature used to verify future messages 150 | """ 151 | params: ZKParameters # Reference ZK Parameters 152 | signature: int # The public key derived from your original secret 153 | 154 | 155 | #### noknow.core.ZKProof: 156 | A cryptograpgic proof that can be verified against a signature. 157 | 158 | class ZKProof(NamedTuple): 159 | """ 160 | Non-deterministic cryptographic zero-knowledge proof that can be verified to ensure the 161 | private key used to create the proof is the same key used to generate the signature 162 | """ 163 | params: ZKParameters # Reference ZK Parameters 164 | c: int # The hash of the signed data and random point, R 165 | m: int # The offset from the secret `r` (`R=r*g`) from c * Hash(secret) 166 | 167 | 168 | #### noknow.core.ZKData 169 | Wrapper that contains a proof and the necessary data to validate the proof against a signature. 170 | 171 | class ZKData(NamedTuple): 172 | """ 173 | Wrapper to contain data and a signed proof using the data 174 | """ 175 | data: Union[str, bytes, int] 176 | proof: ZKProof 177 | 178 | ### ZK 179 | 180 | The `ZK` class is the central component of `NoKnow` and its state (defined by `ZKParameters`) should be inherently known to both the Client (Prover) and Server (Verifier). 181 | 182 | #### instance methods 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 202 | 203 | 204 | 205 | 206 | 207 | 208 |
MethodParametersRolePurpose
create_signaturesecret: Union[str, bytes]ProverCreate a cryptographic signature derived from the value secret to be generated during initial registration and stored for subsequent challenge proofs
signsecret: Union[str, bytes]
data: Union[str, bytes, int]
ProverCreate a ZKData object using the secret and any additional data 201 |
verifychallenge: Union[ZKData, ZKProof]
signature: ZKSignature
data: Optional[Union[str, bytes, int]]
VerifierVerify the user-provided challenge against the stored signature and randomly generated token to verify the validity of the challenge
209 | 210 | ## Install 211 | 212 | `NoKnow` is available from PyPi! Simply run: 213 | 214 | pip install -U noknow 215 | 216 | ## Example Usage 217 | TODO: Include example usage 218 | 219 | #### Example 1 220 | 221 | """ 222 | Extremely simple example of NoKnow ZK Proof implementation 223 | """ 224 | from getpass import getpass 225 | from noknow.core import ZK, ZKSignature, ZKParameters, ZKData, ZKProof 226 | from queue import Queue 227 | from threading import Thread 228 | 229 | 230 | def client(iq: Queue, oq: Queue): 231 | client_zk = ZK.new(curve_name="secp256k1", hash_alg="sha3_256") 232 | 233 | # Create signature and send to server 234 | signature = client_zk.create_signature(getpass("Enter Password: ")) 235 | oq.put(signature.dump()) 236 | 237 | # Receive the token from the server 238 | token = iq.get() 239 | 240 | # Create a proof that signs the provided token and sends to server 241 | proof = client_zk.sign(getpass("Enter Password Again: "), token).dump() 242 | 243 | # Send the token and proof to the server 244 | oq.put(proof) 245 | 246 | # Wait for server response! 247 | print("Success!" if iq.get() else "Failure!") 248 | 249 | 250 | def server(iq: Queue, oq: Queue): 251 | # Set up server component 252 | server_password = "SecretServerPassword" 253 | server_zk = ZK.new(curve_name="secp384r1", hash_alg="sha3_512") 254 | server_signature: ZKSignature = server_zk.create_signature("SecureServerPassword") 255 | 256 | # Load the received signature from the Client 257 | sig = iq.get() 258 | client_signature = ZKSignature.load(sig) 259 | client_zk = ZK(client_signature.params) 260 | 261 | # Create a signed token and send to the client 262 | token = server_zk.sign("SecureServerPassword", client_zk.token()) 263 | oq.put(token.dump(separator=":")) 264 | 265 | # Get the token from the client 266 | proof = ZKData.load(iq.get()) 267 | token = ZKData.load(proof.data, ":") 268 | 269 | # In this example, the server signs the token so it can be sure it has not been modified 270 | if not server_zk.verify(token, server_signature): 271 | oq.put(False) 272 | else: 273 | oq.put(client_zk.verify(proof, client_signature, data=token)) 274 | 275 | 276 | def main(): 277 | q1, q2 = Queue(), Queue() 278 | threads = [ 279 | Thread(target=client, args=(q1, q2)), 280 | Thread(target=server, args=(q2, q1)), 281 | ] 282 | for func in [Thread.start, Thread.join]: 283 | for thread in threads: 284 | func(thread) 285 | 286 | 287 | if __name__ == "__main__": 288 | main() 289 | -------------------------------------------------------------------------------- /cli.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodiesHQ/noknow-python/542d1351336d53252adc17f90783fe2478fb30db/cli.py -------------------------------------------------------------------------------- /noknow/__init__.py: -------------------------------------------------------------------------------- 1 | from noknow import utils 2 | from noknow.core import ZKParameters, ZKProof, ZKSignature, ZKData, ZK 3 | 4 | 5 | __version__ = "0.5.0" 6 | __all__ = [ 7 | "utils", "ZKParameters", "ZKSignature", "ZKProof", "ZKData", "ZK", 8 | ] 9 | -------------------------------------------------------------------------------- /noknow/core.py: -------------------------------------------------------------------------------- 1 | """ 2 | noknow/core.py: Provides the interface for using Zero-Knowledge Proofs 3 | 4 | Implementation of a Schnorr signature with a primary focus on user authentication. 5 | 6 | Signatures are public keys that can be stored with no wary of hacks or credential breaches 7 | Proofs are validations that the signer of the data also knows the password which created the signature. 8 | """ 9 | 10 | from datetime import datetime as dt, timedelta as td 11 | from random import SystemRandom 12 | from typing import NamedTuple, Union 13 | 14 | import hashlib 15 | import json 16 | import secrets 17 | import traceback 18 | 19 | from noknow.data import ZKParameters, ZKSignature, ZKProof, ZKData, dump 20 | from noknow.utils import ( 21 | to_bytes, to_str, bytes_to_int, int_to_bytes, b64e, b64d, 22 | hash_numeric, hash_data, mod, curve_by_name, 23 | ) 24 | 25 | from ecpy.curves import Curve, Point 26 | 27 | import jwt 28 | 29 | # Secure random generation 30 | random = SystemRandom() 31 | 32 | __all__ = [ 33 | "ZKParameters", "ZKSignature", "ZKProof", "ZKData", "ZK", 34 | ] 35 | 36 | class ZK: 37 | """ 38 | Simple implementation of Schnorr's protocol to create and validate proofs 39 | """ 40 | def __init__(self, 41 | parameters: ZKParameters, 42 | jwt_secret: bytes = None, 43 | jwt_alg: str = "HB2S", 44 | jwt_iss: str = "noknow"): 45 | """ 46 | Initialize the curve with the given parameters 47 | """ 48 | self._curve = curve_by_name(parameters.curve) 49 | if not self._curve: 50 | raise ValueError("The curve '{}' is invalid".format(parameters.curve)) 51 | self._params = parameters 52 | self._bits = self._curve.field.bit_length() 53 | # self._mask = (1 << self._bits) - 1 54 | self._jwt_secret = jwt_secret 55 | self._jwt_alg = jwt_alg 56 | self._jwt_iss = jwt_iss 57 | 58 | def jwt(self, signature: ZKSignature, exp=td(seconds=10)): 59 | """ 60 | Generate a signed JWT containing the signature, salt, and parameters 61 | 62 | This token can be signed and subsequently passed into zk.login to 63 | validate both the schnorr signature and JWT integrity 64 | """ 65 | if self._jwt_secret: 66 | now = dt.utcnow() 67 | return to_str(jwt.encode({ 68 | "signature": dump(signature), 69 | "iat": now, "nbf": now, "exp": now + exp, "iss": self._jwt_iss, 70 | }, self._jwt_secret, algorithm=self._jwt_alg)) 71 | 72 | def verify_jwt(self, tok) -> Union[dict, None]: 73 | """ 74 | Verify the validity a JWT token 75 | """ 76 | if self._jwt_secret: 77 | try: 78 | return jwt.decode( 79 | to_str(tok), self._jwt_secret, 80 | iss=self._jwt_iss, algorithms=[self._jwt_alg], 81 | ) 82 | except (jwt.exceptions.ExpiredSignatureError, jwt.exceptions.DecodeError) as e: 83 | traceback.print_exc() 84 | pass 85 | except Exception as e: 86 | traceback.print_exc() 87 | 88 | @property 89 | def params(self) -> ZKParameters: 90 | return self._params 91 | 92 | @property 93 | def salt(self) -> bytes: 94 | return self._params.salt 95 | 96 | @property 97 | def curve(self) -> Curve: 98 | return self._curve 99 | 100 | @staticmethod 101 | def new(curve_name: str = "Ed25519", hash_alg: str = "blake2b", 102 | jwt_secret: bytes = None, jwt_alg = "HB2B", 103 | salt_size: int = 16): 104 | """ 105 | Create a new instance of ZK with the provided parameters 106 | """ 107 | 108 | curve = curve_by_name(curve_name) 109 | if curve is None: 110 | raise ValueError("Invalid Curve Name") 111 | 112 | return ZK( 113 | ZKParameters( 114 | alg=hash_alg, 115 | curve=curve_name, 116 | salt=secrets.token_bytes(salt_size), 117 | ), 118 | jwt_secret=jwt_secret, 119 | jwt_alg=jwt_alg, 120 | ) 121 | 122 | def _to_point(self, value: Union[int, bytes, ZKSignature]): 123 | """ 124 | Convert a value from bytes to a point on the provided curve 125 | """ 126 | point: Point = self.curve.decode_point(to_bytes( 127 | value.signature if isinstance(value, ZKSignature) else value 128 | )) 129 | point.recover() 130 | return point 131 | 132 | def token(self) -> bytes: 133 | """ 134 | Return a random token of a size comparable to the curve field 135 | """ 136 | return secrets.token_bytes( 137 | (self._bits + 7) >> 3 138 | ) 139 | 140 | def hash(self, *values) -> int: 141 | """ 142 | Hash the values provided modulo the curve order 143 | """ 144 | return mod(hash_numeric(*[ 145 | v for v in values if v is not None 146 | ], self.salt, alg=self.params.alg), self.curve.order) 147 | 148 | def create_signature(self, secret: Union[str, bytes]) -> ZKSignature: 149 | return ZKSignature( 150 | params=self.params, 151 | signature=to_bytes( 152 | self.hash(secret) * self.curve.generator), 153 | ) 154 | 155 | def create_proof(self, secret: Union[str, bytes], data: Union[int, str, bytes]=None) -> ZKProof: 156 | key = self.hash(secret) # Create private signing key 157 | r = secrets.randbits(self._bits) # Generate a random number of size comparable to the curve 158 | R = r * self.curve.generator # Random point whose discrete log, `r`, is known 159 | c = self.hash(data, R) # Hash the data and random point 160 | m = mod(r - (c * key), self.curve.order) # Send offset between discrete log of R from c*x mod curve order 161 | return ZKProof(params=self.params, c=int_to_bytes(c), m=int_to_bytes(m)) 162 | 163 | def sign(self, secret: Union[str, bytes], data: Union[int, str, bytes]) -> ZKData: 164 | """ 165 | Construct a proof given the data and secret password used in 166 | the original signature generation 167 | """ 168 | data = to_str(data) 169 | return ZKData( 170 | data=data, 171 | proof=self.create_proof(secret, data), 172 | ) 173 | 174 | @staticmethod 175 | def signature_is_valid(signature: ZKSignature) -> bool: 176 | """ 177 | Verify that a signature is a valid point on the provided curve 178 | """ 179 | try: 180 | zk = ZK(signature.params) 181 | return zk.curve.is_on_curve(zk._to_point(signature)) 182 | except: 183 | return False 184 | 185 | def verify(self, 186 | challenge: Union[ZKData, ZKProof], 187 | signature: ZKSignature, 188 | data: Union[str, bytes, int]="") -> bool: 189 | if isinstance(challenge, ZKProof): 190 | data, proof = data, challenge 191 | elif isinstance(challenge, ZKData): 192 | data, proof = challenge.data, challenge.proof 193 | else: 194 | raise TypeError("Invalid challenge type provided") 195 | c = bytes_to_int(proof.c) 196 | p: Point = (bytes_to_int(proof.m) * self.curve.generator) \ 197 | + (c * self._to_point(signature)) 198 | return c == self.hash(data, p) 199 | 200 | def login(self, login_data: ZKData) -> bool: 201 | """ 202 | Login Data should be a signed JWT token containing the original signature 203 | produced by zk.jwt(). 204 | 205 | Example: 206 | signature = zk.create_signature("MyPassword") 207 | 208 | # To initially log in: 209 | zk.sign("MyPassword", zk.jwt(signature)) 210 | """ 211 | data = self.verify_jwt(login_data.data) 212 | return data and self.verify( 213 | login_data, 214 | ZKSignature.from_json(data.get("signature")), 215 | ) 216 | -------------------------------------------------------------------------------- /noknow/data.py: -------------------------------------------------------------------------------- 1 | """ 2 | Dataclasses and JSON interaction for objects used throughout NoKnow 3 | """ 4 | 5 | from dataclasses import dataclass, field 6 | from dataclasses_json import dataclass_json, config 7 | from noknow.utils.convert import b64e, b64d 8 | 9 | 10 | __all__ = [ 11 | "dump", "ZKParameters", "ZKSignature", "ZKProof", "ZKData" 12 | ] 13 | 14 | 15 | def dump(dc): 16 | """ 17 | Dump a JSON Dataclass to compressed JSON 18 | """ 19 | return dc.to_json(separators=(",", ":")) 20 | 21 | 22 | @dataclass_json 23 | @dataclass 24 | class ZKParameters: 25 | """ 26 | Parameters used to construct a ZK instance using a hashing scheme, 27 | a standard elliptic curve name, and a random salt 28 | """ 29 | alg: str # Hashing algorithm name 30 | curve: str # Standard Elliptic Curve name to use 31 | salt: bytes = field( # Random salt for the state 32 | metadata=config(encoder=b64e, decoder=b64d), 33 | ) 34 | 35 | 36 | @dataclass_json 37 | @dataclass 38 | class ZKSignature: 39 | """ 40 | Cryptographic public signature used to verify future messages 41 | """ 42 | params: ZKParameters # Reference ZK Parameters 43 | signature: bytes = field( # The public key derived from your original secret 44 | metadata=config(encoder=b64e, decoder=b64d), 45 | ) 46 | 47 | 48 | @dataclass_json 49 | @dataclass 50 | class ZKProof: 51 | """ 52 | Cryptographic proof that can be verified to ensure the private key used to create 53 | the proof is the same key used to generate the signature 54 | """ 55 | params: ZKParameters # Reference ZK Parameters 56 | c: bytes = field( # The hash of the signed data and random point, R 57 | metadata=config(encoder=b64e, decoder=b64d), 58 | ) 59 | m: bytes = field( # The offset from the secret `r` (`R=r*g`) from c * Hash(secret) 60 | metadata=config(encoder=b64e, decoder=b64d), 61 | ) 62 | 63 | 64 | @dataclass_json 65 | @dataclass 66 | class ZKData: 67 | """ 68 | Wrapper to contain data and a signed proof using the data 69 | """ 70 | data: bytes = field( # Signed data 71 | metadata=config(encoder=b64e, decoder=b64d), 72 | ) 73 | proof: ZKProof 74 | -------------------------------------------------------------------------------- /noknow/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from noknow.utils import convert, crypto 2 | 3 | from noknow.utils.convert import * 4 | from noknow.utils.crypto import * 5 | 6 | __all__ = [ 7 | "convert", "crypto", *convert.__all__, *crypto.__all__, 8 | ] 9 | -------------------------------------------------------------------------------- /noknow/utils/convert.py: -------------------------------------------------------------------------------- 1 | """ 2 | Methods used for conversion between types and encodings 3 | """ 4 | 5 | from base64 import b64encode, b64decode 6 | from dataclasses import dataclass, asdict, is_dataclass 7 | from typing import Union 8 | 9 | from ecpy.curves import Point 10 | 11 | 12 | __all__ = [ 13 | "to_bytes", "to_str", "bytes_to_int", "int_to_bytes", "b64e", "b64d", 14 | ] 15 | 16 | 17 | def bytes_to_int(value: Union[str, bytes, bytearray, Point]) -> int: 18 | """ 19 | Convert any value to an integer from the big endian bytes representation 20 | """ 21 | return int.from_bytes(to_bytes(value), byteorder="big") 22 | 23 | def int_to_bytes(value: int) -> bytes: 24 | """ 25 | Convert an integer value to bytes in big endian representation 26 | """ 27 | return value.to_bytes((value.bit_length() + 7) // 8, byteorder="big") 28 | 29 | def b64e(data, strip=True) -> str: 30 | """ 31 | Encode in base64, optionally strip padding 32 | """ 33 | return to_str(b64encode(to_bytes(data))).rstrip("=" if strip else None) 34 | 35 | def b64d(data, pad=True) -> bytes: 36 | """ 37 | Decode base64 to bytes, append padding just in case 38 | """ 39 | return b64decode(to_bytes(data) + b"===" if pad else b"") 40 | 41 | def to_bytes(data, encoding="utf-8", errors="replace") -> bytes: 42 | """ 43 | Convert data to bytes representation 44 | """ 45 | if isinstance(data, bytearray): 46 | return bytes(data) 47 | if isinstance(data, bytes): 48 | return data 49 | if isinstance(data, str): 50 | return data.encode(encoding=encoding, errors=errors) 51 | if isinstance(data, int): 52 | return int_to_bytes(data) 53 | if isinstance(data, Point): 54 | c = data.curve 55 | from ecpy.curves import MontgomeryCurve, WeierstrassCurve, TwistedEdwardCurve 56 | if isinstance(c, (MontgomeryCurve, TwistedEdwardCurve)): 57 | return bytes(c.encode_point(data)) 58 | if isinstance(c, WeierstrassCurve): 59 | return bytes(c.encode_point(data, compressed=True)) 60 | raise TypeError("Unknown Curve Type") 61 | print("UNTYPED:", type(data), "\n", data) 62 | return bytes(data) 63 | 64 | 65 | def to_str(data, encoding="utf-8", errors="strict") -> str: 66 | """ 67 | Convert to string representaiton of objects 68 | """ 69 | if isinstance(data, str): 70 | return data 71 | if is_dataclass(data): 72 | return data.to_json(separators=(",", ":")) 73 | if isinstance(data, bytes): 74 | return data.decode(encoding=encoding, errors=errors) 75 | return str(data) 76 | -------------------------------------------------------------------------------- /noknow/utils/crypto.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Generator 2 | import codecs 3 | import hashlib 4 | import random 5 | 6 | from noknow.utils import convert 7 | 8 | from ecpy.curves import Curve 9 | from ecpy.curves import Point 10 | from jwt.algorithms import HMACAlgorithm 11 | from jwt import register_algorithm 12 | 13 | __all__ = [ 14 | "curve_by_name", "mod", "hash_data", "hash_numeric", 15 | ] 16 | 17 | _HASH_TYPES = { 18 | name: getattr(hashlib, name) for name in ( 19 | "md5", "sha1", "sha224", "sha256", "sha512", "sha3_224", 20 | "sha3_256", "sha3_384", "sha3_512", "blake2b", "blake2s", 21 | ) 22 | } 23 | 24 | # Register new JWT algorithms with supported hashlib algorithms 25 | register_algorithm("HS3_224", HMACAlgorithm(hashlib.sha3_224)) 26 | register_algorithm("HS3_256", HMACAlgorithm(hashlib.sha3_256)) 27 | register_algorithm("HS3_384", HMACAlgorithm(hashlib.sha3_384)) 28 | register_algorithm("HS3_512", HMACAlgorithm(hashlib.sha3_512)) 29 | register_algorithm("HB2S", HMACAlgorithm(hashlib.blake2s)) 30 | register_algorithm("HB2B", HMACAlgorithm(hashlib.blake2b)) 31 | 32 | def curve_by_name(name: str) -> Curve: 33 | """ 34 | Get curve by name, case-insensitive 35 | """ 36 | valid_names = Curve.get_curve_names() 37 | for valid_name in valid_names: 38 | if valid_name.lower() == name.lower(): 39 | return Curve.get_curve(valid_name) 40 | return None 41 | 42 | def mod(a: int, b: int) -> int: 43 | """ 44 | Return a mod b, account for positive/negative numbers 45 | """ 46 | return (a % b + b) % b 47 | 48 | def hash_data(*values: Union[str, bytes, bytearray, int, Point], alg="sha3_256") -> bytes: 49 | """ 50 | Convert all provided values to bytes, and return the digest in bytes 51 | """ 52 | if alg not in _HASH_TYPES: 53 | raise NotImplementedError(f"Hash algorithm '{alg}' is not supported") 54 | return _HASH_TYPES[alg](b"".join(map(convert.to_bytes, values))).digest() 55 | 56 | 57 | def hash_numeric(*values: Union[str, bytes, bytearray, int, Point], alg="sha3_256") -> int: 58 | """ 59 | Compute the cryptographic hash of the provided values and return the digest in integer form 60 | """ 61 | return convert.bytes_to_int(hash_data(*values, alg=alg)) 62 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ECPy==0.10.0 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from setuptools import setup, find_packages 3 | from noknow import __version__ as noknow_version 4 | 5 | setup(name="noknow", 6 | version=noknow_version, 7 | packages=find_packages(), 8 | install_requires=["ecpy"], 9 | description="Non-Interactive Zero-Knowledge Proof Implementation in Pure Python", 10 | long_description=open("README.md", "r", encoding="utf-8").read(), 11 | long_description_content_type="text/markdown", 12 | author="Austin Archer", 13 | author_email="aarcher73k@gmail.com", 14 | url="https://github.com/GoodiesHQ/noknow-python/", 15 | classifiers = [ 16 | "License :: OSI Approved :: MIT License", 17 | "Topic :: Security :: Cryptography", 18 | ] 19 | ) 20 | 21 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from pprint import pprint 2 | from queue import Queue 3 | from threading import Thread 4 | import json 5 | import random 6 | 7 | from noknow import ZK, ZKData, ZKParameters, ZKProof, ZKSignature, utils 8 | 9 | 10 | def show(x, title=None): 11 | if title: 12 | print(title) 13 | try: 14 | pprint(json.loads(utils.to_str(x))) 15 | except: 16 | pprint(utils.to_str(x)) 17 | print() 18 | 19 | def client(parameters: ZKParameters, qi: Queue, qo: Queue): 20 | password = b"Secr3tP@ssw0rd!" 21 | 22 | # Step 1) User Registration 23 | zk = ZK(parameters) 24 | sig = zk.create_signature(password) 25 | qo.put(sig) 26 | show(sig, "Client Signature:") 27 | 28 | # Step 3) User signs the provided token 29 | token = qi.get() 30 | qi.task_done() 31 | show(token, "Challenge Token:") 32 | 33 | data = zk.sign(password, token) 34 | qo.put(data) 35 | show(data, "Client Login Data:") 36 | 37 | if qi.get(): 38 | print("Login Successful!") 39 | qi.task_done() 40 | 41 | def server(parameters: ZKParameters, qi: Queue, qo: Queue): 42 | jwt_secret = utils.b64d(b"nKxAQ50yOKormsKWLSG0XR+uh0CudkKsDZ7A3kyqOO8=") 43 | 44 | zk = ZK(parameters, jwt_secret) 45 | client_sig = qi.get() 46 | qi.task_done() 47 | 48 | # Step 2) Server generates a token usable for authentication 49 | qo.put(zk.jwt(client_sig)) 50 | 51 | # Step 4) Server validates the integrity of the signed JWT and Schnorr signature 52 | login_data = qi.get() 53 | qi.task_done() 54 | x = zk.login(login_data) 55 | qo.put(x) 56 | 57 | def main(): 58 | curves = [ 59 | "secp256r1", "secp256k1", "secp224k1", "secp224r1", 60 | "secp192k1", "secp192r1", "secp160k1", "secp160r1", "secp160r2", 61 | "Brainpool-p256r1", "Brainpool-p256t1", "Brainpool-p224r1", 62 | "Brainpool-p224t1", "Brainpool-p192r1", "Brainpool-p192t1", 63 | "Brainpool-p160r1", "Brainpool-p160t1", "NIST-P256", "NIST-P224", 64 | "NIST-P192", "Ed25519", 65 | # "Curve25519", # does not work 66 | ] 67 | 68 | hash_algs = [ 69 | "blake2s", "blake2b", "md5", "sha3_256", "sha3_512", 70 | ] 71 | 72 | jwt_algs = [ 73 | "HS3_256", "HS3_512", "HB2S", "HB2B", 74 | ] 75 | 76 | zk = ZK.new( 77 | curve_name=random.choice(curves), 78 | hash_alg=random.choice(hash_algs), 79 | salt_size=16, 80 | ) 81 | parameters = zk.params 82 | q1, q2 = Queue(), Queue() 83 | 84 | threads = [ 85 | Thread(target=client, args=(parameters, q1, q2)), 86 | Thread(target=server, args=(parameters, q2, q1)), 87 | ] 88 | 89 | for func in (Thread.start, Thread.join): 90 | for thread in threads: 91 | func(thread) 92 | q1.join() 93 | q2.join() 94 | 95 | if __name__ == "__main__": 96 | main() --------------------------------------------------------------------------------