├── .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 |

8 |
9 |
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 | Method |
186 | Parameters |
187 | Role |
188 | Purpose |
189 |
190 |
191 | create_signature |
192 | secret: Union[str, bytes] |
193 | Prover |
194 | Create a cryptographic signature derived from the value secret to be generated during initial registration and stored for subsequent challenge proofs |
195 |
196 |
197 | sign |
198 | secret: Union[str, bytes] data: Union[str, bytes, int] |
199 | Prover |
200 | Create a ZKData object using the secret and any additional data
201 | |
202 |
203 | verify |
204 | challenge: Union[ZKData, ZKProof] signature: ZKSignature data: Optional[Union[str, bytes, int]] |
205 | Verifier |
206 | Verify the user-provided challenge against the stored signature and randomly generated token to verify the validity of the challenge |
207 |
208 |
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()
--------------------------------------------------------------------------------