├── tests ├── __init__.py ├── signatureDer.txt ├── publicKey.pem ├── message.txt ├── privateKey.pem ├── testRandom.py ├── testSignature.py ├── testPrivateKey.py ├── testEcdsa.py ├── testSignatureWithRecoveryId.py ├── testOpenSSL.py ├── testPublicKey.py ├── testCompPubKey.py └── testCurve.py ├── ellipticcurve ├── utils │ ├── __init__.py │ ├── file.py │ ├── integer.py │ ├── pem.py │ ├── binary.py │ ├── oid.py │ ├── compatibility.py │ └── der.py ├── __init__.py ├── point.py ├── signature.py ├── ecdsa.py ├── privateKey.py ├── curve.py ├── publicKey.py └── math.py ├── MANIFEST.in ├── .gitignore ├── .travis.yml ├── setup.py ├── LICENSE ├── CHANGELOG.md └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ellipticcurve/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include README.md 3 | prune tests -------------------------------------------------------------------------------- /tests/signatureDer.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/starkbank/ecdsa-python/HEAD/tests/signatureDer.txt -------------------------------------------------------------------------------- /tests/publicKey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE/nvHu/SQQaos9TUljQsUuKI15Zr5SabP 3 | rbwtbfT/408rkVVzq8vAisbBRmpeRREXj5aog/Mq8RrdYy75W9q/Ig== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /tests/message.txt: -------------------------------------------------------------------------------- 1 | { 2 | "transaction": { 3 | "amount": 50000, 4 | "receiver": "", 5 | "description": "Sample transfer between workspaces", 6 | "externalId": "123456", 7 | "tags": ["john", "smith"] 8 | } 9 | } -------------------------------------------------------------------------------- /ellipticcurve/utils/file.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class File: 4 | 5 | @classmethod 6 | def read(cls, path, mode="r"): 7 | with open(path, mode) as blob: 8 | content = blob.read() 9 | return content 10 | -------------------------------------------------------------------------------- /ellipticcurve/__init__.py: -------------------------------------------------------------------------------- 1 | from .utils.compatibility import * 2 | from .privateKey import PrivateKey 3 | from .publicKey import PublicKey 4 | from .signature import Signature 5 | from .utils.file import File 6 | from .ecdsa import Ecdsa 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.idea/* 3 | *.DS_Store 4 | 5 | # folder used by virtualenv 6 | env 7 | .env 8 | venv 9 | .venv 10 | 11 | # ignore info from all dependencies 12 | *.dist-info 13 | *.egg-info 14 | 15 | # ignore dist folder 16 | dist -------------------------------------------------------------------------------- /tests/privateKey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PARAMETERS----- 2 | BgUrgQQACg== 3 | -----END EC PARAMETERS----- 4 | -----BEGIN EC PRIVATE KEY----- 5 | MHQCAQEEIODvZuS34wFbt0X53+P5EnSj6tMjfVK01dD1dgDH02RzoAcGBSuBBAAK 6 | oUQDQgAE/nvHu/SQQaos9TUljQsUuKI15Zr5SabPrbwtbfT/408rkVVzq8vAisbB 7 | RmpeRREXj5aog/Mq8RrdYy75W9q/Ig== 8 | -----END EC PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /ellipticcurve/point.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Point: 4 | 5 | def __init__(self, x=0, y=0, z=0): 6 | self.x = x 7 | self.y = y 8 | self.z = z 9 | 10 | def __str__(self): 11 | return "({x}, {y}, {z})".format(x=self.x, y=self.y, z=self.z) 12 | 13 | def isAtInfinity(self): 14 | return self.y == 0 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | matrix: 4 | include: 5 | - python: "2.7" 6 | - python: "3.4" 7 | - python: "3.5" 8 | - python: "3.6" 9 | - python: "3.7" 10 | - python: "3.8" 11 | - python: "3.9-dev" 12 | - python: "pypy2.7-6.0" 13 | - python: "pypy3.5-6.0" 14 | allow_failures: 15 | - python: "3.9-dev" 16 | 17 | script: 18 | - python -m unittest discover 19 | -------------------------------------------------------------------------------- /ellipticcurve/utils/integer.py: -------------------------------------------------------------------------------- 1 | from random import SystemRandom 2 | 3 | 4 | class RandomInteger: 5 | 6 | @classmethod 7 | def between(cls, min, max): 8 | """ 9 | Return integer x in the range: min <= x <= max 10 | 11 | :param min: minimum value of the integer 12 | :param max: maximum value of the integer 13 | :return: 14 | """ 15 | 16 | return SystemRandom().randrange(min, max + 1) 17 | -------------------------------------------------------------------------------- /ellipticcurve/utils/pem.py: -------------------------------------------------------------------------------- 1 | from re import search 2 | 3 | 4 | def getPemContent(pem, template): 5 | pattern = template.format(content="(.*)") 6 | return search("".join(pattern.splitlines()), "".join(pem.splitlines())).group(1) 7 | 8 | 9 | def createPem(content, template): 10 | lines = [ 11 | content[start:start + 64] 12 | for start in range(0, len(content), 64) 13 | ] 14 | return template.format(content="\n".join(lines)) 15 | -------------------------------------------------------------------------------- /tests/testRandom.py: -------------------------------------------------------------------------------- 1 | from unittest.case import TestCase 2 | from ellipticcurve import Ecdsa, Signature, PublicKey, PrivateKey 3 | 4 | 5 | class RandomTest(TestCase): 6 | 7 | def testMany(self): 8 | for _ in range(1000): 9 | privateKey1 = PrivateKey() 10 | publicKey1 = privateKey1.publicKey() 11 | 12 | privateKeyPem = privateKey1.toPem() 13 | publicKeyPem = publicKey1.toPem() 14 | 15 | privateKey2 = PrivateKey.fromPem(privateKeyPem) 16 | publicKey2 = PublicKey.fromPem(publicKeyPem) 17 | 18 | message = "test" 19 | 20 | signatureBase64 = Ecdsa.sign(message=message, privateKey=privateKey2).toBase64() 21 | signature = Signature.fromBase64(signatureBase64) 22 | 23 | self.assertTrue(Ecdsa.verify(message=message, signature=signature, publicKey=publicKey2)) 24 | -------------------------------------------------------------------------------- /tests/testSignature.py: -------------------------------------------------------------------------------- 1 | from unittest.case import TestCase 2 | from ellipticcurve import Ecdsa, PrivateKey, Signature 3 | 4 | 5 | class SignatureTest(TestCase): 6 | 7 | def testDerConversion(self): 8 | privateKey = PrivateKey() 9 | message = "This is a text message" 10 | 11 | signature1 = Ecdsa.sign(message, privateKey) 12 | 13 | der = signature1.toDer() 14 | signature2 = Signature.fromDer(der) 15 | 16 | self.assertEqual(signature1.r, signature2.r) 17 | self.assertEqual(signature1.s, signature2.s) 18 | 19 | def testBase64Conversion(self): 20 | privateKey = PrivateKey() 21 | message = "This is a text message" 22 | 23 | signature1 = Ecdsa.sign(message, privateKey) 24 | 25 | base64 = signature1.toBase64() 26 | 27 | signature2 = Signature.fromBase64(base64) 28 | 29 | self.assertEqual(signature1.r, signature2.r) 30 | self.assertEqual(signature1.s, signature2.s) 31 | -------------------------------------------------------------------------------- /ellipticcurve/utils/binary.py: -------------------------------------------------------------------------------- 1 | from base64 import b64encode, b64decode 2 | from .compatibility import safeHexFromBinary, safeBinaryFromHex, toString 3 | 4 | 5 | def hexFromInt(number): 6 | hexadecimal = "{0:x}".format(number) 7 | if len(hexadecimal) % 2 == 1: 8 | hexadecimal = "0" + hexadecimal 9 | return hexadecimal 10 | 11 | 12 | def intFromHex(hexadecimal): 13 | return int(hexadecimal, 16) 14 | 15 | 16 | def hexFromByteString(byteString): 17 | return safeHexFromBinary(byteString) 18 | 19 | 20 | def byteStringFromHex(hexadecimal): 21 | return safeBinaryFromHex(hexadecimal) 22 | 23 | 24 | def numberFromByteString(byteString): 25 | return intFromHex(hexFromByteString(byteString)) 26 | 27 | 28 | def base64FromByteString(byteString): 29 | return toString(b64encode(byteString)) 30 | 31 | 32 | def byteStringFromBase64(base64String): 33 | return b64decode(base64String) 34 | 35 | 36 | def bitsFromHex(hexadecimal): 37 | return format(intFromHex(hexadecimal), 'b').zfill(4 * len(hexadecimal)) 38 | -------------------------------------------------------------------------------- /tests/testPrivateKey.py: -------------------------------------------------------------------------------- 1 | from unittest.case import TestCase 2 | from ellipticcurve.privateKey import PrivateKey 3 | 4 | 5 | class PrivateKeyTest(TestCase): 6 | 7 | def testPemConversion(self): 8 | privateKey1 = PrivateKey() 9 | pem = privateKey1.toPem() 10 | privateKey2 = PrivateKey.fromPem(pem) 11 | self.assertEqual(privateKey1.secret, privateKey2.secret) 12 | self.assertEqual(privateKey1.curve, privateKey2.curve) 13 | 14 | def testDerConversion(self): 15 | privateKey1 = PrivateKey() 16 | der = privateKey1.toDer() 17 | privateKey2 = PrivateKey.fromDer(der) 18 | self.assertEqual(privateKey1.secret, privateKey2.secret) 19 | self.assertEqual(privateKey1.curve, privateKey2.curve) 20 | 21 | def testStringConversion(self): 22 | privateKey1 = PrivateKey() 23 | string = privateKey1.toString() 24 | privateKey2 = PrivateKey.fromString(string) 25 | self.assertEqual(privateKey1.secret, privateKey2.secret) 26 | self.assertEqual(privateKey1.curve, privateKey2.curve) 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | from setuptools import setup, find_packages 3 | 4 | 5 | with open(path.join(path.dirname(__file__), "README.md")) as readme: 6 | README = readme.read() 7 | 8 | 9 | setup( 10 | name="starkbank-ecdsa", 11 | packages=find_packages(exclude=["tests"]), 12 | include_package_data=True, 13 | description="A lightweight and fast pure python ECDSA library", 14 | long_description=README, 15 | long_description_content_type="text/markdown", 16 | license="MIT License", 17 | url="https://github.com/starkbank/ecdsa-python.git", 18 | author="Stark Bank", 19 | author_email="developers@starkbank.com", 20 | keywords=["ecdsa", "elliptic curve", "elliptic", "curve", "stark bank", "starkbank", "cryptograph", "secp256k1", "prime256v1"], 21 | version="2.2.0" 22 | ) 23 | 24 | 25 | ### Create a source distribution: 26 | 27 | #Run ```python setup.py sdist``` inside the project directory. 28 | 29 | ### Install twine: 30 | 31 | #```pip install twine``` 32 | 33 | ### Upload package to pypi: 34 | 35 | #```twine upload dist/*``` 36 | 37 | -------------------------------------------------------------------------------- /tests/testEcdsa.py: -------------------------------------------------------------------------------- 1 | from unittest.case import TestCase 2 | from ellipticcurve import Ecdsa, PrivateKey, Signature 3 | 4 | 5 | class EcdsaTest(TestCase): 6 | 7 | def testVerifyRightMessage(self): 8 | privateKey = PrivateKey() 9 | publicKey = privateKey.publicKey() 10 | 11 | message = "This is the right message" 12 | 13 | signature = Ecdsa.sign(message, privateKey) 14 | 15 | self.assertTrue(Ecdsa.verify(message, signature, publicKey)) 16 | 17 | def testVerifyWrongMessage(self): 18 | privateKey = PrivateKey() 19 | publicKey = privateKey.publicKey() 20 | 21 | message1 = "This is the right message" 22 | message2 = "This is the wrong message" 23 | 24 | signature = Ecdsa.sign(message1, privateKey) 25 | 26 | self.assertFalse(Ecdsa.verify(message2, signature, publicKey)) 27 | 28 | def testZeroSignature(self): 29 | privateKey = PrivateKey() 30 | publicKey = privateKey.publicKey() 31 | 32 | message2 = "This is the wrong message" 33 | 34 | self.assertFalse(Ecdsa.verify(message2, Signature(0, 0), publicKey)) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Stark Bank S.A. 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 | -------------------------------------------------------------------------------- /ellipticcurve/utils/oid.py: -------------------------------------------------------------------------------- 1 | from .binary import intFromHex, hexFromInt 2 | 3 | 4 | def oidFromHex(hexadecimal): 5 | firstByte, remainingBytes = hexadecimal[:2], hexadecimal[2:] 6 | firstByteInt = intFromHex(firstByte) 7 | oid = [firstByteInt // 40, firstByteInt % 40] 8 | oidInt = 0 9 | while len(remainingBytes) > 0: 10 | byte, remainingBytes = remainingBytes[0:2], remainingBytes[2:] 11 | byteInt = intFromHex(byte) 12 | if byteInt >= 128: 13 | oidInt = (128 * oidInt) + (byteInt - 128) 14 | continue 15 | oidInt = (128 * oidInt) + byteInt 16 | oid.append(oidInt) 17 | oidInt = 0 18 | return oid 19 | 20 | 21 | def oidToHex(oid): 22 | hexadecimal = hexFromInt(40 * oid[0] + oid[1]) 23 | for number in oid[2:]: 24 | hexadecimal += _oidNumberToHex(number) 25 | return hexadecimal 26 | 27 | 28 | def _oidNumberToHex(number): 29 | hexadecimal = "" 30 | endDelta = 0 31 | while number > 0: 32 | hexadecimal = hexFromInt((number % 128) + endDelta) + hexadecimal 33 | number //= 128 34 | endDelta = 128 35 | return hexadecimal or "00" 36 | -------------------------------------------------------------------------------- /tests/testSignatureWithRecoveryId.py: -------------------------------------------------------------------------------- 1 | from unittest.case import TestCase 2 | from ellipticcurve import Ecdsa, PrivateKey, Signature 3 | 4 | 5 | class SignatureTest(TestCase): 6 | 7 | def testDerConversion(self): 8 | privateKey = PrivateKey() 9 | message = "This is a text message" 10 | 11 | signature1 = Ecdsa.sign(message, privateKey) 12 | 13 | der = signature1.toDer(withRecoveryId=True) 14 | signature2 = Signature.fromDer(der, recoveryByte=True) 15 | 16 | self.assertEqual(signature1.r, signature2.r) 17 | self.assertEqual(signature1.s, signature2.s) 18 | self.assertEqual(signature1.recoveryId, signature2.recoveryId) 19 | 20 | def testBase64Conversion(self): 21 | privateKey = PrivateKey() 22 | message = "This is a text message" 23 | 24 | signature1 = Ecdsa.sign(message, privateKey) 25 | 26 | base64 = signature1.toBase64(withRecoveryId=True) 27 | 28 | signature2 = Signature.fromBase64(base64, recoveryByte=True) 29 | 30 | self.assertEqual(signature1.r, signature2.r) 31 | self.assertEqual(signature1.s, signature2.s) 32 | self.assertEqual(signature1.recoveryId, signature2.recoveryId) 33 | -------------------------------------------------------------------------------- /ellipticcurve/utils/compatibility.py: -------------------------------------------------------------------------------- 1 | from sys import version_info as pyVersion 2 | from binascii import hexlify, unhexlify 3 | 4 | 5 | if pyVersion.major == 3: 6 | # py3 constants and conversion functions 7 | 8 | stringTypes = (str,) 9 | intTypes = (int, float) 10 | 11 | def toString(string, encoding="utf-8"): 12 | return string.decode(encoding) 13 | 14 | def toBytes(string, encoding="utf-8"): 15 | return string.encode(encoding) 16 | 17 | def safeBinaryFromHex(hexadecimal): 18 | if len(hexadecimal) % 2 == 1: 19 | hexadecimal = "0" + hexadecimal 20 | return unhexlify(hexadecimal) 21 | 22 | def safeHexFromBinary(byteString): 23 | return toString(hexlify(byteString)) 24 | else: 25 | # py2 constants and conversion functions 26 | 27 | stringTypes = (str, unicode) 28 | intTypes = (int, float, long) 29 | 30 | def toString(string, encoding="utf-8"): 31 | return string 32 | 33 | def toBytes(string, encoding="utf-8"): 34 | return string 35 | 36 | def safeBinaryFromHex(hexadecimal): 37 | return unhexlify(hexadecimal) 38 | 39 | def safeHexFromBinary(byteString): 40 | return hexlify(byteString) 41 | -------------------------------------------------------------------------------- /tests/testOpenSSL.py: -------------------------------------------------------------------------------- 1 | from unittest.case import TestCase 2 | from ellipticcurve import Ecdsa, PrivateKey, PublicKey, Signature, File 3 | 4 | 5 | class OpensslTest(TestCase): 6 | 7 | def testAssign(self): 8 | # Generated by: openssl ecparam -name secp256k1 -genkey -out privateKey.pem 9 | privateKeyPem = File.read("privateKey.pem") 10 | 11 | privateKey = PrivateKey.fromPem(privateKeyPem) 12 | 13 | message = File.read("message.txt") 14 | 15 | signature = Ecdsa.sign(message=message, privateKey=privateKey) 16 | 17 | publicKey = privateKey.publicKey() 18 | 19 | self.assertTrue(Ecdsa.verify(message=message, signature=signature, publicKey=publicKey)) 20 | 21 | def testVerifySignature(self): 22 | # openssl ec -in privateKey.pem -pubout -out publicKey.pem 23 | 24 | publicKeyPem = File.read("publicKey.pem") 25 | 26 | # openssl dgst -sha256 -sign privateKey.pem -out signature.binary message.txt 27 | signatureDer = File.read("signatureDer.txt", "rb") 28 | 29 | message = File.read("message.txt") 30 | 31 | publicKey = PublicKey.fromPem(publicKeyPem) 32 | 33 | signature = Signature.fromDer(string=signatureDer) 34 | 35 | self.assertTrue(Ecdsa.verify(message=message, signature=signature, publicKey=publicKey)) 36 | -------------------------------------------------------------------------------- /tests/testPublicKey.py: -------------------------------------------------------------------------------- 1 | from unittest.case import TestCase 2 | from ellipticcurve.privateKey import PrivateKey 3 | from ellipticcurve.publicKey import PublicKey 4 | from ellipticcurve.utils.compatibility import * 5 | 6 | 7 | class PublicKeyTest(TestCase): 8 | 9 | def testPemConversion(self): 10 | privateKey = PrivateKey() 11 | publicKey1 = privateKey.publicKey() 12 | pem = publicKey1.toPem() 13 | publicKey2 = PublicKey.fromPem(pem) 14 | self.assertEqual(publicKey1.point.x, publicKey2.point.x) 15 | self.assertEqual(publicKey1.point.y, publicKey2.point.y) 16 | self.assertEqual(publicKey1.curve, publicKey2.curve) 17 | 18 | def testDerConversion(self): 19 | privateKey = PrivateKey() 20 | publicKey1 = privateKey.publicKey() 21 | der = publicKey1.toDer() 22 | publicKey2 = PublicKey.fromDer(der) 23 | self.assertEqual(publicKey1.point.x, publicKey2.point.x) 24 | self.assertEqual(publicKey1.point.y, publicKey2.point.y) 25 | self.assertEqual(publicKey1.curve, publicKey2.curve) 26 | 27 | def testStringConversion(self): 28 | privateKey = PrivateKey() 29 | publicKey1 = privateKey.publicKey() 30 | string = publicKey1.toString() 31 | publicKey2 = PublicKey.fromString(toBytes(string)) 32 | self.assertEqual(publicKey1.point.x, publicKey2.point.x) 33 | self.assertEqual(publicKey1.point.y, publicKey2.point.y) 34 | self.assertEqual(publicKey1.curve, publicKey2.curve) 35 | -------------------------------------------------------------------------------- /ellipticcurve/signature.py: -------------------------------------------------------------------------------- 1 | from .utils.compatibility import * 2 | from .utils.der import parse, encodeConstructed, encodePrimitive, DerFieldType 3 | from .utils.binary import hexFromByteString, byteStringFromHex, base64FromByteString, byteStringFromBase64 4 | 5 | 6 | class Signature: 7 | 8 | def __init__(self, r, s, recoveryId=None): 9 | self.r = r 10 | self.s = s 11 | self.recoveryId = recoveryId 12 | 13 | def toDer(self, withRecoveryId=False): 14 | hexadecimal = self._toString() 15 | encodedSequence = byteStringFromHex(hexadecimal) 16 | if not withRecoveryId: 17 | return encodedSequence 18 | return toBytes(chr(27 + self.recoveryId)) + encodedSequence 19 | 20 | def toBase64(self, withRecoveryId=False): 21 | return base64FromByteString(self.toDer(withRecoveryId)) 22 | 23 | @classmethod 24 | def fromDer(cls, string, recoveryByte=False): 25 | recoveryId = None 26 | if recoveryByte: 27 | recoveryId = string[0] if isinstance(string[0], intTypes) else ord(string[0]) 28 | recoveryId -= 27 29 | string = string[1:] 30 | 31 | hexadecimal = hexFromByteString(string) 32 | return cls._fromString(string=hexadecimal, recoveryId=recoveryId) 33 | 34 | @classmethod 35 | def fromBase64(cls, string, recoveryByte=False): 36 | der = byteStringFromBase64(string) 37 | return cls.fromDer(der, recoveryByte) 38 | 39 | def _toString(self): 40 | return encodeConstructed( 41 | encodePrimitive(DerFieldType.integer, self.r), 42 | encodePrimitive(DerFieldType.integer, self.s), 43 | ) 44 | 45 | @classmethod 46 | def _fromString(cls, string, recoveryId=None): 47 | r, s = parse(string)[0] 48 | return Signature(r=r, s=s, recoveryId=recoveryId) 49 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to the following versioning pattern: 7 | 8 | Given a version number MAJOR.MINOR.PATCH, increment: 9 | 10 | - MAJOR version when **breaking changes** are introduced; 11 | - MINOR version when **backwards compatible changes** are introduced; 12 | - PATCH version when backwards compatible bug **fixes** are implemented. 13 | 14 | 15 | ## [Unreleased] 16 | 17 | ## [2.2.0] - 2022-10-24 18 | ### Added 19 | - PublicKey.toCompressed() function to dump a public key in compressed format 20 | - PublicKey.fromCompressed() function to read a public key in compressed format 21 | 22 | ## [2.1.0] - 2022-09-20 23 | ### Added 24 | - curve.add() function to dynamically add curves to the library 25 | ### Changed 26 | - curve.getCurveByOid() to curve.getByOid() 27 | 28 | ## [2.0.3] - 2021-11-24 29 | ### Fixed 30 | - OID integer encoding when single number has more than 2 bytes 31 | 32 | ## [2.0.2] - 2021-11-09 33 | ### Fixed 34 | - Missing point at infinity checks on signature and public key verifications 35 | 36 | ## [2.0.1] - 2021-11-04 37 | ### Fixed 38 | - Signature r and s range check 39 | 40 | ## [2.0.0] - 2021-10-08 41 | ### Added 42 | - root imports: from ellipticcurve import PrivateKey, PublicKey, Signature, Ecdsa, File 43 | ### Changed 44 | - return type of toDer() methods from str to bytes 45 | - internal DER parsing structure for better maintainability, translatability and usability 46 | 47 | ## [1.1.1] - 2021-06-06 48 | ### Fixed 49 | - unstable results on certain curves due to missing modulo operator on signature verification 50 | 51 | ## [1.1.0] - 2020-09-04 52 | ### Added 53 | - recoveryId generation and encoding in Signatures 54 | 55 | ## [1.0.0] - 2020-04-13 56 | ### Added 57 | - first official version 58 | -------------------------------------------------------------------------------- /ellipticcurve/ecdsa.py: -------------------------------------------------------------------------------- 1 | from hashlib import sha256 2 | from .signature import Signature 3 | from .math import Math 4 | from .utils.integer import RandomInteger 5 | from .utils.binary import numberFromByteString 6 | from .utils.compatibility import * 7 | 8 | 9 | class Ecdsa: 10 | 11 | @classmethod 12 | def sign(cls, message, privateKey, hashfunc=sha256): 13 | byteMessage = hashfunc(toBytes(message)).digest() 14 | numberMessage = numberFromByteString(byteMessage) 15 | curve = privateKey.curve 16 | 17 | r, s, randSignPoint = 0, 0, None 18 | while r == 0 or s == 0: 19 | randNum = RandomInteger.between(1, curve.N - 1) 20 | randSignPoint = Math.multiply(curve.G, n=randNum, A=curve.A, P=curve.P, N=curve.N) 21 | r = randSignPoint.x % curve.N 22 | s = ((numberMessage + r * privateKey.secret) * (Math.inv(randNum, curve.N))) % curve.N 23 | recoveryId = randSignPoint.y & 1 24 | if randSignPoint.y > curve.N: 25 | recoveryId += 2 26 | 27 | return Signature(r=r, s=s, recoveryId=recoveryId) 28 | 29 | @classmethod 30 | def verify(cls, message, signature, publicKey, hashfunc=sha256): 31 | byteMessage = hashfunc(toBytes(message)).digest() 32 | numberMessage = numberFromByteString(byteMessage) 33 | curve = publicKey.curve 34 | r = signature.r 35 | s = signature.s 36 | if not 1 <= r <= curve.N - 1: 37 | return False 38 | if not 1 <= s <= curve.N - 1: 39 | return False 40 | inv = Math.inv(s, curve.N) 41 | u1 = Math.multiply(curve.G, n=(numberMessage * inv) % curve.N, N=curve.N, A=curve.A, P=curve.P) 42 | u2 = Math.multiply(publicKey.point, n=(r * inv) % curve.N, N=curve.N, A=curve.A, P=curve.P) 43 | v = Math.add(u1, u2, A=curve.A, P=curve.P) 44 | if v.isAtInfinity(): 45 | return False 46 | return v.x % curve.N == r 47 | -------------------------------------------------------------------------------- /tests/testCompPubKey.py: -------------------------------------------------------------------------------- 1 | from unittest.case import TestCase 2 | from ellipticcurve import PrivateKey, PublicKey 3 | 4 | 5 | class CompPubKeyTest(TestCase): 6 | 7 | def testBatch(self): 8 | for _ in range(1000): 9 | privateKey = PrivateKey() 10 | publicKey = privateKey.publicKey() 11 | publicKeyString = publicKey.toCompressed() 12 | 13 | recoveredPublicKey = PublicKey.fromCompressed(publicKeyString, publicKey.curve) 14 | 15 | self.assertEqual(publicKey.point.x, recoveredPublicKey.point.x) 16 | self.assertEqual(publicKey.point.y, recoveredPublicKey.point.y) 17 | 18 | def testFromCompressedEven(self): 19 | publicKeyCompressed = "0252972572d465d016d4c501887b8df303eee3ed602c056b1eb09260dfa0da0ab2" 20 | publicKey = PublicKey.fromCompressed(publicKeyCompressed) 21 | self.assertEqual(publicKey.toPem(), "\n-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEUpclctRl0BbUxQGIe43zA+7j7WAsBWse\nsJJg36DaCrKIdC9NyX2e22/ZRrq8AC/fsG8myvEXuUBe15J1dj/bHA==\n-----END PUBLIC KEY-----\n") 22 | 23 | def testFromCompressedOdd(self): 24 | publicKeyCompressed = "0318ed2e1ec629e2d3dae7be1103d4f911c24e0c80e70038f5eb5548245c475f50" 25 | publicKey = PublicKey.fromCompressed(publicKeyCompressed) 26 | self.assertEqual(publicKey.toPem(), "\n-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEGO0uHsYp4tPa574RA9T5EcJODIDnADj1\n61VIJFxHX1BMIg0B4cpBnLG6SzOTthXpndIKpr8HEHj3D9lJAI50EQ==\n-----END PUBLIC KEY-----\n") 27 | 28 | def testToCompressedEven(self): 29 | publicKey = PublicKey.fromPem("-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEUpclctRl0BbUxQGIe43zA+7j7WAsBWse\nsJJg36DaCrKIdC9NyX2e22/ZRrq8AC/fsG8myvEXuUBe15J1dj/bHA==\n-----END PUBLIC KEY-----") 30 | publicKeyCompressed = publicKey.toCompressed() 31 | self.assertEqual(publicKeyCompressed, "0252972572d465d016d4c501887b8df303eee3ed602c056b1eb09260dfa0da0ab2") 32 | 33 | def testToCompressedOdd(self): 34 | publicKey = PublicKey.fromPem("-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEGO0uHsYp4tPa574RA9T5EcJODIDnADj1\n61VIJFxHX1BMIg0B4cpBnLG6SzOTthXpndIKpr8HEHj3D9lJAI50EQ==\n-----END PUBLIC KEY-----") 35 | publicKeyCompressed = publicKey.toCompressed() 36 | self.assertEqual(publicKeyCompressed, "0318ed2e1ec629e2d3dae7be1103d4f911c24e0c80e70038f5eb5548245c475f50") 37 | -------------------------------------------------------------------------------- /ellipticcurve/privateKey.py: -------------------------------------------------------------------------------- 1 | from .math import Math 2 | from .utils.integer import RandomInteger 3 | from .utils.pem import getPemContent, createPem 4 | from .utils.binary import hexFromByteString, byteStringFromHex, intFromHex, base64FromByteString, byteStringFromBase64 5 | from .utils.der import hexFromInt, parse, encodeConstructed, DerFieldType, encodePrimitive 6 | from .curve import secp256k1, getByOid 7 | from .publicKey import PublicKey 8 | 9 | 10 | class PrivateKey: 11 | 12 | def __init__(self, curve=secp256k1, secret=None): 13 | self.curve = curve 14 | self.secret = secret or RandomInteger.between(1, curve.N - 1) 15 | 16 | def publicKey(self): 17 | curve = self.curve 18 | publicPoint = Math.multiply( 19 | p=curve.G, 20 | n=self.secret, 21 | N=curve.N, 22 | A=curve.A, 23 | P=curve.P, 24 | ) 25 | return PublicKey(point=publicPoint, curve=curve) 26 | 27 | def toString(self): 28 | return hexFromInt(self.secret) 29 | 30 | def toDer(self): 31 | publicKeyString = self.publicKey().toString(encoded=True) 32 | hexadecimal = encodeConstructed( 33 | encodePrimitive(DerFieldType.integer, 1), 34 | encodePrimitive(DerFieldType.octetString, hexFromInt(self.secret)), 35 | encodePrimitive(DerFieldType.oidContainer, encodePrimitive(DerFieldType.object, self.curve.oid)), 36 | encodePrimitive(DerFieldType.publicKeyPointContainer, encodePrimitive(DerFieldType.bitString, publicKeyString)) 37 | ) 38 | return byteStringFromHex(hexadecimal) 39 | 40 | def toPem(self): 41 | der = self.toDer() 42 | return createPem(content=base64FromByteString(der), template=_pemTemplate) 43 | 44 | @classmethod 45 | def fromPem(cls, string): 46 | privateKeyPem = getPemContent(pem=string, template=_pemTemplate) 47 | return cls.fromDer(byteStringFromBase64(privateKeyPem)) 48 | 49 | @classmethod 50 | def fromDer(cls, string): 51 | hexadecimal = hexFromByteString(string) 52 | privateKeyFlag, secretHex, curveData, publicKeyString = parse(hexadecimal)[0] 53 | if privateKeyFlag != 1: 54 | raise Exception("Private keys should start with a '1' flag, but a '{flag}' was found instead".format( 55 | flag=privateKeyFlag 56 | )) 57 | curve = getByOid(curveData[0]) 58 | privateKey = cls.fromString(string=secretHex, curve=curve) 59 | if privateKey.publicKey().toString(encoded=True) != publicKeyString[0]: 60 | raise Exception("The public key described inside the private key file doesn't match the actual public key of the pair") 61 | return privateKey 62 | 63 | @classmethod 64 | def fromString(cls, string, curve=secp256k1): 65 | return PrivateKey(secret=intFromHex(string), curve=curve) 66 | 67 | 68 | _pemTemplate = """ 69 | -----BEGIN EC PRIVATE KEY----- 70 | {content} 71 | -----END EC PRIVATE KEY----- 72 | """ 73 | -------------------------------------------------------------------------------- /ellipticcurve/curve.py: -------------------------------------------------------------------------------- 1 | # 2 | # Elliptic Curve Equation 3 | # 4 | # y^2 = x^3 + A*x + B (mod P) 5 | # 6 | from .math import Math 7 | from .point import Point 8 | 9 | 10 | class CurveFp: 11 | 12 | def __init__(self, A, B, P, N, Gx, Gy, name, oid, nistName=None): 13 | self.A = A 14 | self.B = B 15 | self.P = P 16 | self.N = N 17 | self.G = Point(Gx, Gy) 18 | self.name = name 19 | self.nistName = nistName 20 | self.oid = oid # ASN.1 Object Identifier 21 | 22 | def contains(self, p): 23 | """ 24 | Verify if the point `p` is on the curve 25 | 26 | :param p: Point p = Point(x, y) 27 | :return: boolean 28 | """ 29 | if not 0 <= p.x <= self.P - 1: 30 | return False 31 | if not 0 <= p.y <= self.P - 1: 32 | return False 33 | if (p.y**2 - (p.x**3 + self.A * p.x + self.B)) % self.P != 0: 34 | return False 35 | return True 36 | 37 | def length(self): 38 | return (1 + len("%x" % self.N)) // 2 39 | 40 | def y(self, x, isEven): 41 | ySquared = (pow(x, 3, self.P) + self.A * x + self.B) % self.P 42 | y = Math.modularSquareRoot(ySquared, self.P) 43 | if isEven != (y % 2 == 0): 44 | y = self.P - y 45 | return y 46 | 47 | 48 | _curvesByOid = {tuple(curve.oid): curve for curve in []} 49 | 50 | 51 | def add(curve): 52 | _curvesByOid[tuple(curve.oid)] = curve 53 | 54 | 55 | def getByOid(oid): 56 | if oid not in _curvesByOid: 57 | raise Exception("Unknown curve with oid {oid}; The following are registered: {names}".format( 58 | oid=".".join([str(number) for number in oid]), 59 | names=", ".join([curve.name for curve in _curvesByOid.values()]), 60 | )) 61 | return _curvesByOid[oid] 62 | 63 | 64 | secp256k1 = CurveFp( 65 | name="secp256k1", 66 | A=0x0000000000000000000000000000000000000000000000000000000000000000, 67 | B=0x0000000000000000000000000000000000000000000000000000000000000007, 68 | P=0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f, 69 | N=0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141, 70 | Gx=0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798, 71 | Gy=0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8, 72 | oid=[1, 3, 132, 0, 10] 73 | ) 74 | 75 | prime256v1 = CurveFp( 76 | name="prime256v1", 77 | nistName="P-256", 78 | A=0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc, 79 | B=0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b, 80 | P=0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff, 81 | N=0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551, 82 | Gx=0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296, 83 | Gy=0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5, 84 | oid=[1, 2, 840, 10045, 3, 1, 7], 85 | ) 86 | 87 | p256 = prime256v1 88 | 89 | add(secp256k1) 90 | add(prime256v1) 91 | -------------------------------------------------------------------------------- /tests/testCurve.py: -------------------------------------------------------------------------------- 1 | from unittest.case import TestCase 2 | from ellipticcurve import curve, PublicKey, Signature, Ecdsa, PrivateKey 3 | 4 | 5 | class CurveTest(TestCase): 6 | 7 | def testSupportedCurve(self): 8 | newCurve = curve.CurveFp( 9 | name="secp256k1", 10 | A=0x0000000000000000000000000000000000000000000000000000000000000000, 11 | B=0x0000000000000000000000000000000000000000000000000000000000000007, 12 | P=0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f, 13 | N=0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141, 14 | Gx=0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798, 15 | Gy=0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8, 16 | oid=[1, 3, 132, 0, 10] 17 | ) 18 | privateKey1 = PrivateKey(curve=newCurve) 19 | publicKey1 = privateKey1.publicKey() 20 | 21 | privateKeyPem = privateKey1.toPem() 22 | publicKeyPem = publicKey1.toPem() 23 | 24 | privateKey2 = PrivateKey.fromPem(privateKeyPem) 25 | publicKey2 = PublicKey.fromPem(publicKeyPem) 26 | 27 | message = "test" 28 | 29 | signatureBase64 = Ecdsa.sign(message=message, privateKey=privateKey2).toBase64() 30 | signature = Signature.fromBase64(signatureBase64) 31 | 32 | self.assertTrue(Ecdsa.verify(message=message, signature=signature, publicKey=publicKey2)) 33 | 34 | def testAddNewCurve(self): 35 | newCurve = curve.CurveFp( 36 | name="frp256v1", 37 | A=0xf1fd178c0b3ad58f10126de8ce42435b3961adbcabc8ca6de8fcf353d86e9c00, 38 | B=0xee353fca5428a9300d4aba754a44c00fdfec0c9ae4b1a1803075ed967b7bb73f, 39 | P=0xf1fd178c0b3ad58f10126de8ce42435b3961adbcabc8ca6de8fcf353d86e9c03, 40 | N=0xf1fd178c0b3ad58f10126de8ce42435b53dc67e140d2bf941ffdd459c6d655e1, 41 | Gx=0xb6b3d4c356c139eb31183d4749d423958c27d2dcaf98b70164c97a2dd98f5cff, 42 | Gy=0x6142e0f7c8b204911f9271f0f3ecef8c2701c307e8e4c9e183115a1554062cfb, 43 | oid=[1, 2, 250, 1, 223, 101, 256, 1] 44 | ) 45 | curve.add(newCurve) 46 | privateKey1 = PrivateKey(curve=newCurve) 47 | publicKey1 = privateKey1.publicKey() 48 | 49 | privateKeyPem = privateKey1.toPem() 50 | publicKeyPem = publicKey1.toPem() 51 | 52 | privateKey2 = PrivateKey.fromPem(privateKeyPem) 53 | publicKey2 = PublicKey.fromPem(publicKeyPem) 54 | 55 | message = "test" 56 | 57 | signatureBase64 = Ecdsa.sign(message=message, privateKey=privateKey2).toBase64() 58 | signature = Signature.fromBase64(signatureBase64) 59 | 60 | self.assertTrue(Ecdsa.verify(message=message, signature=signature, publicKey=publicKey2)) 61 | 62 | def testUnsupportedCurve(self): 63 | newCurve = curve.CurveFp( 64 | name="brainpoolP256t1", 65 | A=0xa9fb57dba1eea9bc3e660a909d838d726e3bf623d52620282013481d1f6e5374, 66 | B=0x662c61c430d84ea4fe66a7733d0b76b7bf93ebc4af2f49256ae58101fee92b04, 67 | P=0xa9fb57dba1eea9bc3e660a909d838d726e3bf623d52620282013481d1f6e5377, 68 | N=0xa9fb57dba1eea9bc3e660a909d838d718c397aa3b561a6f7901e0e82974856a7, 69 | Gx=0xa3e8eb3cc1cfe7b7732213b23a656149afa142c47aafbc2b79a191562e1305f4, 70 | Gy=0x2d996c823439c56d7f7b22e14644417e69bcb6de39d027001dabe8f35b25c9be, 71 | oid=[1, 3, 36, 3, 3, 2, 8, 1, 1, 8] 72 | ) 73 | 74 | privateKeyPem = PrivateKey(curve=newCurve).toPem() 75 | publicKeyPem = PrivateKey(curve=newCurve).publicKey().toPem() 76 | 77 | with self.assertRaises(Exception) as context: 78 | privateKey = PrivateKey.fromPem(privateKeyPem) 79 | self.assertTrue('Unknown curve' in str(context.exception)) 80 | 81 | with self.assertRaises(Exception) as context: 82 | publicKey = PublicKey.fromPem(publicKeyPem) 83 | self.assertTrue('Unknown curve' in str(context.exception)) 84 | 85 | 86 | -------------------------------------------------------------------------------- /ellipticcurve/publicKey.py: -------------------------------------------------------------------------------- 1 | from .math import Math 2 | from .point import Point 3 | from .curve import secp256k1, getByOid 4 | from .utils.pem import getPemContent, createPem 5 | from .utils.der import hexFromInt, parse, DerFieldType, encodeConstructed, encodePrimitive 6 | from .utils.binary import hexFromByteString, byteStringFromHex, intFromHex, base64FromByteString, byteStringFromBase64 7 | 8 | 9 | class PublicKey: 10 | 11 | def __init__(self, point, curve): 12 | self.point = point 13 | self.curve = curve 14 | 15 | def toString(self, encoded=False): 16 | baseLength = 2 * self.curve.length() 17 | xHex = hexFromInt(self.point.x).zfill(baseLength) 18 | yHex = hexFromInt(self.point.y).zfill(baseLength) 19 | string = xHex + yHex 20 | if encoded: 21 | return "0004" + string 22 | return string 23 | 24 | def toCompressed(self): 25 | baseLength = 2 * self.curve.length() 26 | parityTag = _evenTag if self.point.y % 2 == 0 else _oddTag 27 | xHex = hexFromInt(self.point.x).zfill(baseLength) 28 | return parityTag + xHex 29 | 30 | def toDer(self): 31 | hexadecimal = encodeConstructed( 32 | encodeConstructed( 33 | encodePrimitive(DerFieldType.object, _ecdsaPublicKeyOid), 34 | encodePrimitive(DerFieldType.object, self.curve.oid), 35 | ), 36 | encodePrimitive(DerFieldType.bitString, self.toString(encoded=True)), 37 | ) 38 | return byteStringFromHex(hexadecimal) 39 | 40 | def toPem(self): 41 | der = self.toDer() 42 | return createPem(content=base64FromByteString(der), template=_pemTemplate) 43 | 44 | @classmethod 45 | def fromPem(cls, string): 46 | publicKeyPem = getPemContent(pem=string, template=_pemTemplate) 47 | return cls.fromDer(byteStringFromBase64(publicKeyPem)) 48 | 49 | @classmethod 50 | def fromDer(cls, string): 51 | hexadecimal = hexFromByteString(string) 52 | curveData, pointString = parse(hexadecimal)[0] 53 | publicKeyOid, curveOid = curveData 54 | if publicKeyOid != _ecdsaPublicKeyOid: 55 | raise Exception("The Public Key Object Identifier (OID) should be {ecdsaPublicKeyOid}, but {actualOid} was found instead".format( 56 | ecdsaPublicKeyOid=_ecdsaPublicKeyOid, 57 | actualOid=publicKeyOid, 58 | )) 59 | curve = getByOid(curveOid) 60 | return cls.fromString(string=pointString, curve=curve) 61 | 62 | @classmethod 63 | def fromString(cls, string, curve=secp256k1, validatePoint=True): 64 | baseLength = 2 * curve.length() 65 | if len(string) > 2 * baseLength and string[:4] == "0004": 66 | string = string[4:] 67 | 68 | xs = string[:baseLength] 69 | ys = string[baseLength:] 70 | 71 | p = Point( 72 | x=intFromHex(xs), 73 | y=intFromHex(ys), 74 | ) 75 | publicKey = PublicKey(point=p, curve=curve) 76 | if not validatePoint: 77 | return publicKey 78 | if p.isAtInfinity(): 79 | raise Exception("Public Key point is at infinity") 80 | if not curve.contains(p): 81 | raise Exception("Point ({x},{y}) is not valid for curve {name}".format(x=p.x, y=p.y, name=curve.name)) 82 | if not Math.multiply(p=p, n=curve.N, N=curve.N, A=curve.A, P=curve.P).isAtInfinity(): 83 | raise Exception("Point ({x},{y}) * {name}.N is not at infinity".format(x=p.x, y=p.y, name=curve.name)) 84 | return publicKey 85 | 86 | @classmethod 87 | def fromCompressed(cls, string, curve=secp256k1): 88 | parityTag, xHex = string[:2], string[2:] 89 | if parityTag not in [_evenTag, _oddTag]: 90 | raise Exception("Compressed string should start with 02 or 03") 91 | x = intFromHex(xHex) 92 | y = curve.y(x, isEven=parityTag == _evenTag) 93 | return cls(point=Point(x, y), curve=curve) 94 | 95 | 96 | _evenTag = "02" 97 | _oddTag = "03" 98 | 99 | 100 | _ecdsaPublicKeyOid = (1, 2, 840, 10045, 2, 1) 101 | 102 | 103 | _pemTemplate = """ 104 | -----BEGIN PUBLIC KEY----- 105 | {content} 106 | -----END PUBLIC KEY----- 107 | """ 108 | -------------------------------------------------------------------------------- /ellipticcurve/utils/der.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from .oid import oidToHex, oidFromHex 3 | from .binary import hexFromInt, intFromHex, byteStringFromHex, bitsFromHex 4 | 5 | 6 | class DerFieldType: 7 | 8 | integer = "integer" 9 | bitString = "bitString" 10 | octetString = "octetString" 11 | null = "null" 12 | object = "object" 13 | printableString = "printableString" 14 | utcTime = "utcTime" 15 | sequence = "sequence" 16 | set = "set" 17 | oidContainer = "oidContainer" 18 | publicKeyPointContainer = "publicKeyPointContainer" 19 | 20 | 21 | _hexTagToType = { 22 | "02": DerFieldType.integer, 23 | "03": DerFieldType.bitString, 24 | "04": DerFieldType.octetString, 25 | "05": DerFieldType.null, 26 | "06": DerFieldType.object, 27 | "13": DerFieldType.printableString, 28 | "17": DerFieldType.utcTime, 29 | "30": DerFieldType.sequence, 30 | "31": DerFieldType.set, 31 | "a0": DerFieldType.oidContainer, 32 | "a1": DerFieldType.publicKeyPointContainer, 33 | } 34 | _typeToHexTag = {v: k for k, v in _hexTagToType.items()} 35 | 36 | 37 | def encodeConstructed(*encodedValues): 38 | return encodePrimitive(DerFieldType.sequence, "".join(encodedValues)) 39 | 40 | 41 | def encodePrimitive(tagType, value): 42 | if tagType == DerFieldType.integer: 43 | value = _encodeInteger(value) 44 | if tagType == DerFieldType.object: 45 | value = oidToHex(value) 46 | return "{tag}{size}{value}".format(tag=_typeToHexTag[tagType], size=_generateLengthBytes(value), value=value) 47 | 48 | 49 | def parse(hexadecimal): 50 | if not hexadecimal: 51 | return [] 52 | typeByte, hexadecimal = hexadecimal[:2], hexadecimal[2:] 53 | length, lengthBytes = _readLengthBytes(hexadecimal) 54 | content, hexadecimal = hexadecimal[lengthBytes: lengthBytes + length], hexadecimal[lengthBytes + length:] 55 | if len(content) < length: 56 | raise Exception("missing bytes in DER parse") 57 | 58 | tagData = _getTagData(typeByte) 59 | if tagData["isConstructed"]: 60 | content = parse(content) 61 | 62 | valueParser = { 63 | DerFieldType.null: _parseNull, 64 | DerFieldType.object: _parseOid, 65 | DerFieldType.utcTime: _parseTime, 66 | DerFieldType.integer: _parseInteger, 67 | DerFieldType.printableString: _parseString, 68 | }.get(tagData["type"], _parseAny) 69 | return [valueParser(content)] + parse(hexadecimal) 70 | 71 | 72 | def _parseAny(hexadecimal): 73 | return hexadecimal 74 | 75 | 76 | def _parseOid(hexadecimal): 77 | return tuple(oidFromHex(hexadecimal)) 78 | 79 | 80 | def _parseTime(hexadecimal): 81 | string = _parseString(hexadecimal) 82 | return datetime.strptime(string, "%y%m%d%H%M%SZ") 83 | 84 | 85 | def _parseString(hexadecimal): 86 | return byteStringFromHex(hexadecimal).decode() 87 | 88 | 89 | def _parseNull(_content): 90 | return None 91 | 92 | 93 | def _parseInteger(hexadecimal): 94 | integer = intFromHex(hexadecimal) 95 | bits = bitsFromHex(hexadecimal[0]) 96 | if bits[0] == "0": # negative numbers are encoded using two's complement 97 | return integer 98 | bitCount = 4 * len(hexadecimal) 99 | return integer - (2 ** bitCount) 100 | 101 | 102 | def _encodeInteger(number): 103 | hexadecimal = hexFromInt(abs(number)) 104 | if number < 0: 105 | bitCount = 4 * len(hexadecimal) 106 | twosComplement = (2 ** bitCount) + number 107 | return hexFromInt(twosComplement) 108 | bits = bitsFromHex(hexadecimal[0]) 109 | if bits[0] == "1": # if first bit was left as 1, number would be parsed as a negative integer with two's complement 110 | hexadecimal = "00" + hexadecimal 111 | return hexadecimal 112 | 113 | 114 | def _readLengthBytes(hexadecimal): 115 | lengthBytes = 2 116 | lengthIndicator = intFromHex(hexadecimal[0:lengthBytes]) 117 | isShortForm = lengthIndicator < 128 # checks if first bit of byte is 1 (a.k.a. short-form) 118 | if isShortForm: 119 | length = lengthIndicator * 2 120 | return length, lengthBytes 121 | 122 | lengthLength = lengthIndicator - 128 # nullifies first bit of byte (only used as long-form flag) 123 | if lengthLength == 0: 124 | raise Exception("indefinite length encoding located in DER") 125 | lengthBytes += 2 * lengthLength 126 | length = intFromHex(hexadecimal[2:lengthBytes]) * 2 127 | return length, lengthBytes 128 | 129 | 130 | def _generateLengthBytes(hexadecimal): 131 | size = len(hexadecimal) // 2 132 | length = hexFromInt(size) 133 | if size < 128: # checks if first bit of byte should be 0 (a.k.a. short-form flag) 134 | return length.zfill(2) 135 | lengthLength = 128 + len(length) // 2 # +128 sets the first bit of the byte as 1 (a.k.a. long-form flag) 136 | return hexFromInt(lengthLength) + length 137 | 138 | 139 | def _getTagData(tag): 140 | bits = bitsFromHex(tag) 141 | bit8, bit7, bit6 = bits[:3] 142 | 143 | tagClass = { 144 | "0": { 145 | "0": "universal", 146 | "1": "application", 147 | }, 148 | "1": { 149 | "0": "context-specific", 150 | "1": "private", 151 | }, 152 | }[bit8][bit7] 153 | isConstructed = bit6 == "1" 154 | 155 | return { 156 | "class": tagClass, 157 | "isConstructed": isConstructed, 158 | "type": _hexTagToType.get(tag), 159 | } 160 | -------------------------------------------------------------------------------- /ellipticcurve/math.py: -------------------------------------------------------------------------------- 1 | from .point import Point 2 | 3 | 4 | class Math: 5 | 6 | @classmethod 7 | def modularSquareRoot(cls, value, prime): 8 | return pow(value, (prime + 1) // 4, prime) 9 | 10 | @classmethod 11 | def multiply(cls, p, n, N, A, P): 12 | """ 13 | Fast way to multily point and scalar in elliptic curves 14 | 15 | :param p: First Point to mutiply 16 | :param n: Scalar to mutiply 17 | :param N: Order of the elliptic curve 18 | :param P: Prime number in the module of the equation Y^2 = X^3 + A*X + B (mod p) 19 | :param A: Coefficient of the first-order term of the equation Y^2 = X^3 + A*X + B (mod p) 20 | :return: Point that represents the sum of First and Second Point 21 | """ 22 | return cls._fromJacobian( 23 | cls._jacobianMultiply(cls._toJacobian(p), n, N, A, P), P 24 | ) 25 | 26 | @classmethod 27 | def add(cls, p, q, A, P): 28 | """ 29 | Fast way to add two points in elliptic curves 30 | 31 | :param p: First Point you want to add 32 | :param q: Second Point you want to add 33 | :param P: Prime number in the module of the equation Y^2 = X^3 + A*X + B (mod p) 34 | :param A: Coefficient of the first-order term of the equation Y^2 = X^3 + A*X + B (mod p) 35 | :return: Point that represents the sum of First and Second Point 36 | """ 37 | return cls._fromJacobian( 38 | cls._jacobianAdd(cls._toJacobian(p), cls._toJacobian(q), A, P), P, 39 | ) 40 | 41 | @classmethod 42 | def inv(cls, x, n): 43 | """ 44 | Extended Euclidean Algorithm. It's the 'division' in elliptic curves 45 | 46 | :param x: Divisor 47 | :param n: Mod for division 48 | :return: Value representing the division 49 | """ 50 | if x == 0: 51 | return 0 52 | 53 | lm = 1 54 | hm = 0 55 | low = x % n 56 | high = n 57 | 58 | while low > 1: 59 | r = high // low 60 | nm = hm - lm * r 61 | nw = high - low * r 62 | high = low 63 | hm = lm 64 | low = nw 65 | lm = nm 66 | 67 | return lm % n 68 | 69 | @classmethod 70 | def _toJacobian(cls, p): 71 | """ 72 | Convert point to Jacobian coordinates 73 | 74 | :param p: First Point you want to add 75 | :return: Point in Jacobian coordinates 76 | """ 77 | return Point(p.x, p.y, 1) 78 | 79 | @classmethod 80 | def _fromJacobian(cls, p, P): 81 | """ 82 | Convert point back from Jacobian coordinates 83 | 84 | :param p: First Point you want to add 85 | :param P: Prime number in the module of the equation Y^2 = X^3 + A*X + B (mod p) 86 | :return: Point in default coordinates 87 | """ 88 | z = cls.inv(p.z, P) 89 | x = (p.x * z ** 2) % P 90 | y = (p.y * z ** 3) % P 91 | 92 | return Point(x, y, 0) 93 | 94 | @classmethod 95 | def _jacobianDouble(cls, p, A, P): 96 | """ 97 | Double a point in elliptic curves 98 | 99 | :param p: Point you want to double 100 | :param P: Prime number in the module of the equation Y^2 = X^3 + A*X + B (mod p) 101 | :param A: Coefficient of the first-order term of the equation Y^2 = X^3 + A*X + B (mod p) 102 | :return: Point that represents the sum of First and Second Point 103 | """ 104 | if p.y == 0: 105 | return Point(0, 0, 0) 106 | 107 | ysq = (p.y ** 2) % P 108 | S = (4 * p.x * ysq) % P 109 | M = (3 * p.x ** 2 + A * p.z ** 4) % P 110 | nx = (M**2 - 2 * S) % P 111 | ny = (M * (S - nx) - 8 * ysq ** 2) % P 112 | nz = (2 * p.y * p.z) % P 113 | 114 | return Point(nx, ny, nz) 115 | 116 | @classmethod 117 | def _jacobianAdd(cls, p, q, A, P): 118 | """ 119 | Add two points in elliptic curves 120 | 121 | :param p: First Point you want to add 122 | :param q: Second Point you want to add 123 | :param P: Prime number in the module of the equation Y^2 = X^3 + A*X + B (mod p) 124 | :param A: Coefficient of the first-order term of the equation Y^2 = X^3 + A*X + B (mod p) 125 | :return: Point that represents the sum of First and Second Point 126 | """ 127 | if p.y == 0: 128 | return q 129 | if q.y == 0: 130 | return p 131 | 132 | U1 = (p.x * q.z ** 2) % P 133 | U2 = (q.x * p.z ** 2) % P 134 | S1 = (p.y * q.z ** 3) % P 135 | S2 = (q.y * p.z ** 3) % P 136 | 137 | if U1 == U2: 138 | if S1 != S2: 139 | return Point(0, 0, 1) 140 | return cls._jacobianDouble(p, A, P) 141 | 142 | H = U2 - U1 143 | R = S2 - S1 144 | H2 = (H * H) % P 145 | H3 = (H * H2) % P 146 | U1H2 = (U1 * H2) % P 147 | nx = (R ** 2 - H3 - 2 * U1H2) % P 148 | ny = (R * (U1H2 - nx) - S1 * H3) % P 149 | nz = (H * p.z * q.z) % P 150 | 151 | return Point(nx, ny, nz) 152 | 153 | @classmethod 154 | def _jacobianMultiply(cls, p, n, N, A, P): 155 | """ 156 | Multily point and scalar in elliptic curves 157 | 158 | :param p: First Point to mutiply 159 | :param n: Scalar to mutiply 160 | :param N: Order of the elliptic curve 161 | :param P: Prime number in the module of the equation Y^2 = X^3 + A*X + B (mod p) 162 | :param A: Coefficient of the first-order term of the equation Y^2 = X^3 + A*X + B (mod p) 163 | :return: Point that represents the sum of First and Second Point 164 | """ 165 | if p.y == 0 or n == 0: 166 | return Point(0, 0, 1) 167 | 168 | if n == 1: 169 | return p 170 | 171 | if n < 0 or n >= N: 172 | return cls._jacobianMultiply(p, n % N, N, A, P) 173 | 174 | if (n % 2) == 0: 175 | return cls._jacobianDouble( 176 | cls._jacobianMultiply(p, n // 2, N, A, P), A, P 177 | ) 178 | 179 | return cls._jacobianAdd( 180 | cls._jacobianDouble(cls._jacobianMultiply(p, n // 2, N, A, P), A, P), p, A, P 181 | ) 182 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## A lightweight and fast pure Python ECDSA 2 | 3 | ### Overview 4 | 5 | We tried other Python libraries such as [python-ecdsa], [fast-ecdsa] and other less famous ones, but we didn't find anything that suited our needs. The first one was pure Python, but it was too slow. The second one mixed Python and C and it was really fast, but we were unable to use it in our current infrastructure, which required pure Python code. 6 | 7 | For this reason, we decided to create something simple, compatible with OpenSSL and fast using elegant math such as Jacobian Coordinates to speed up the ECDSA. Starkbank-ECDSA is fully compatible with Python2 and Python3. 8 | 9 | ### Installation 10 | 11 | To install StarkBank`s ECDSA-Python, run: 12 | 13 | ```sh 14 | pip install starkbank-ecdsa 15 | ``` 16 | 17 | ### Curves 18 | 19 | We currently support `secp256k1`, but you can add more curves to the project. You just need to use the curve.add() function. 20 | 21 | ### Speed 22 | 23 | We ran a test on a MAC Pro i7 2017. The libraries were run 100 times and the averages displayed bellow were obtained: 24 | 25 | | Library | sign | verify | 26 | | ------------------ |:-------------:| -------:| 27 | | [python-ecdsa] | 121.3ms | 65.1ms | 28 | | [fast-ecdsa] | 0.1ms | 0.2ms | 29 | | starkbank-ecdsa | 4.1ms | 7.8ms | 30 | 31 | Our pure Python code cannot compete with C based libraries, but it's `6x faster` to verify and `23x faster` to sign than other pure Python libraries. 32 | 33 | ### Sample Code 34 | 35 | How to sign a json message for [Stark Bank]: 36 | 37 | ```python 38 | from json import dumps 39 | from ellipticcurve.ecdsa import Ecdsa 40 | from ellipticcurve.privateKey import PrivateKey 41 | 42 | 43 | # Generate privateKey from PEM string 44 | privateKey = PrivateKey.fromPem(""" 45 | -----BEGIN EC PARAMETERS----- 46 | BgUrgQQACg== 47 | -----END EC PARAMETERS----- 48 | -----BEGIN EC PRIVATE KEY----- 49 | MHQCAQEEIODvZuS34wFbt0X53+P5EnSj6tMjfVK01dD1dgDH02RzoAcGBSuBBAAK 50 | oUQDQgAE/nvHu/SQQaos9TUljQsUuKI15Zr5SabPrbwtbfT/408rkVVzq8vAisbB 51 | RmpeRREXj5aog/Mq8RrdYy75W9q/Ig== 52 | -----END EC PRIVATE KEY----- 53 | """) 54 | 55 | # Create message from json 56 | message = dumps({ 57 | "transfers": [ 58 | { 59 | "amount": 100000000, 60 | "taxId": "594.739.480-42", 61 | "name": "Daenerys Targaryen Stormborn", 62 | "bankCode": "341", 63 | "branchCode": "2201", 64 | "accountNumber": "76543-8", 65 | "tags": ["daenerys", "targaryen", "transfer-1-external-id"] 66 | } 67 | ] 68 | }) 69 | 70 | signature = Ecdsa.sign(message, privateKey) 71 | 72 | # Generate Signature in base64. This result can be sent to Stark Bank in the request header as the Digital-Signature parameter. 73 | print(signature.toBase64()) 74 | 75 | # To double check if the message matches the signature, do this: 76 | publicKey = privateKey.publicKey() 77 | 78 | print(Ecdsa.verify(message, signature, publicKey)) 79 | 80 | ``` 81 | 82 | Simple use: 83 | 84 | ```python 85 | from ellipticcurve.ecdsa import Ecdsa 86 | from ellipticcurve.privateKey import PrivateKey 87 | 88 | 89 | # Generate new Keys 90 | privateKey = PrivateKey() 91 | publicKey = privateKey.publicKey() 92 | 93 | message = "My test message" 94 | 95 | # Generate Signature 96 | signature = Ecdsa.sign(message, privateKey) 97 | 98 | # To verify if the signature is valid 99 | print(Ecdsa.verify(message, signature, publicKey)) 100 | 101 | ``` 102 | 103 | How to add more curves: 104 | 105 | ```python 106 | from ellipticcurve import curve, PrivateKey, PublicKey 107 | 108 | newCurve = curve.CurveFp( 109 | name="frp256v1", 110 | A=0xf1fd178c0b3ad58f10126de8ce42435b3961adbcabc8ca6de8fcf353d86e9c00, 111 | B=0xee353fca5428a9300d4aba754a44c00fdfec0c9ae4b1a1803075ed967b7bb73f, 112 | P=0xf1fd178c0b3ad58f10126de8ce42435b3961adbcabc8ca6de8fcf353d86e9c03, 113 | N=0xf1fd178c0b3ad58f10126de8ce42435b53dc67e140d2bf941ffdd459c6d655e1, 114 | Gx=0xb6b3d4c356c139eb31183d4749d423958c27d2dcaf98b70164c97a2dd98f5cff, 115 | Gy=0x6142e0f7c8b204911f9271f0f3ecef8c2701c307e8e4c9e183115a1554062cfb, 116 | oid=[1, 2, 250, 1, 223, 101, 256, 1] 117 | ) 118 | 119 | curve.add(newCurve) 120 | 121 | publicKeyPem = """-----BEGIN PUBLIC KEY----- 122 | MFswFQYHKoZIzj0CAQYKKoF6AYFfZYIAAQNCAATeEFFYiQL+HmDYTf+QDmvQmWGD 123 | dRJPqLj11do8okvkSxq2lwB6Ct4aITMlCyg3f1msafc/ROSN/Vgj69bDhZK6 124 | -----END PUBLIC KEY-----""" 125 | 126 | publicKey = PublicKey.fromPem(publicKeyPem) 127 | 128 | print(publicKey.toPem()) 129 | ``` 130 | 131 | How to generate compressed public key: 132 | 133 | ```python 134 | from ellipticcurve import PrivateKey, PublicKey 135 | 136 | privateKey = PrivateKey() 137 | publicKey = privateKey.publicKey() 138 | compressedPublicKey = publicKey.toCompressed() 139 | 140 | print(compressedPublicKey) 141 | ``` 142 | 143 | How to recover a compressed public key: 144 | 145 | ```python 146 | from ellipticcurve import PrivateKey, PublicKey 147 | 148 | compressedPublicKey = "0252972572d465d016d4c501887b8df303eee3ed602c056b1eb09260dfa0da0ab2" 149 | publicKey = PublicKey.fromCompressed(compressedPublicKey) 150 | 151 | print(publicKey.toPem()) 152 | ``` 153 | 154 | ### OpenSSL 155 | 156 | This library is compatible with OpenSSL, so you can use it to generate keys: 157 | 158 | ``` 159 | openssl ecparam -name secp256k1 -genkey -out privateKey.pem 160 | openssl ec -in privateKey.pem -pubout -out publicKey.pem 161 | ``` 162 | 163 | Create a message.txt file and sign it: 164 | 165 | ``` 166 | openssl dgst -sha256 -sign privateKey.pem -out signatureDer.txt message.txt 167 | ``` 168 | 169 | To verify, do this: 170 | 171 | ```python 172 | from ellipticcurve.ecdsa import Ecdsa 173 | from ellipticcurve.signature import Signature 174 | from ellipticcurve.publicKey import PublicKey 175 | from ellipticcurve.utils.file import File 176 | 177 | 178 | publicKeyPem = File.read("publicKey.pem") 179 | signatureDer = File.read("signatureDer.txt", "rb") 180 | message = File.read("message.txt") 181 | 182 | publicKey = PublicKey.fromPem(publicKeyPem) 183 | signature = Signature.fromDer(signatureDer) 184 | 185 | print(Ecdsa.verify(message, signature, publicKey)) 186 | 187 | ``` 188 | 189 | You can also verify it on terminal: 190 | 191 | ``` 192 | openssl dgst -sha256 -verify publicKey.pem -signature signatureDer.txt message.txt 193 | ``` 194 | 195 | NOTE: If you want to create a Digital Signature to use with [Stark Bank], you need to convert the binary signature to base64. 196 | 197 | ``` 198 | openssl base64 -in signatureDer.txt -out signatureBase64.txt 199 | ``` 200 | 201 | You can do the same with this library: 202 | 203 | ```python 204 | from ellipticcurve.signature import Signature 205 | from ellipticcurve.utils.file import File 206 | 207 | 208 | signatureDer = File.read("signatureDer.txt", "rb") 209 | 210 | signature = Signature.fromDer(signatureDer) 211 | 212 | print(signature.toBase64()) 213 | ``` 214 | 215 | ### Run unit tests 216 | 217 | ``` 218 | python3 -m unittest discover 219 | python2 -m unittest discover 220 | ``` 221 | 222 | 223 | [python-ecdsa]: https://github.com/warner/python-ecdsa 224 | [fast-ecdsa]: https://github.com/AntonKueltz/fastecdsa 225 | [Stark Bank]: https://starkbank.com 226 | --------------------------------------------------------------------------------