From e0b795b4d0b1319f07a63579275797c841cc2ed5 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Wed, 8 Jun 2022 19:47:09 +0200 Subject: [PATCH] Added internal python-only implementation of Ed25519 --- RNS/Cryptography/Ed25519.py | 41 +++ RNS/Cryptography/Hashes.py | 20 +- RNS/Cryptography/Proxies.py | 49 ++- RNS/Cryptography/__init__.py | 6 +- RNS/Cryptography/pure25519/__init__.py | 0 RNS/Cryptography/pure25519/_ed25519.py | 58 ++++ RNS/Cryptography/pure25519/basic.py | 368 ++++++++++++++++++++++ RNS/Cryptography/pure25519/ed25519_oop.py | 213 +++++++++++++ RNS/Cryptography/pure25519/eddsa.py | 94 ++++++ RNS/Identity.py | 20 +- RNS/Link.py | 11 +- RNS/_version.py | 2 +- 12 files changed, 845 insertions(+), 37 deletions(-) create mode 100644 RNS/Cryptography/Ed25519.py create mode 100644 RNS/Cryptography/pure25519/__init__.py create mode 100644 RNS/Cryptography/pure25519/_ed25519.py create mode 100644 RNS/Cryptography/pure25519/basic.py create mode 100644 RNS/Cryptography/pure25519/ed25519_oop.py create mode 100644 RNS/Cryptography/pure25519/eddsa.py diff --git a/RNS/Cryptography/Ed25519.py b/RNS/Cryptography/Ed25519.py new file mode 100644 index 0000000..db4b984 --- /dev/null +++ b/RNS/Cryptography/Ed25519.py @@ -0,0 +1,41 @@ +import os +from .pure25519 import ed25519_oop as ed25519 + +class Ed25519PrivateKey: + def __init__(self, seed): + self.seed = seed + self.sk = ed25519.SigningKey(self.seed) + #self.vk = self.sk.get_verifying_key() + + @classmethod + def generate(cls): + return cls.from_private_bytes(os.urandom(32)) + + @classmethod + def from_private_bytes(cls, data): + return cls(seed=data) + + def private_bytes(self): + return self.seed + + def public_key(self): + return Ed25519PublicKey.from_public_bytes(self.sk.vk_s) + + def sign(self, message): + return self.sk.sign(message) + + +class Ed25519PublicKey: + def __init__(self, seed): + self.seed = seed + self.vk = ed25519.VerifyingKey(self.seed) + + @classmethod + def from_public_bytes(cls, data): + return cls(data) + + def public_bytes(self): + return self.vk.to_bytes() + + def verify(self, signature, message): + self.vk.verify(signature, message) diff --git a/RNS/Cryptography/Hashes.py b/RNS/Cryptography/Hashes.py index cea0f87..4bb1db7 100644 --- a/RNS/Cryptography/Hashes.py +++ b/RNS/Cryptography/Hashes.py @@ -1,14 +1,20 @@ import hashlib +""" +The SHA primitives are abstracted here to allow platform- +aware hardware acceleration in the future. Currently only +uses Python's internal SHA-256 implementation. All SHA-256 +calls in RNS end up here. +""" def sha256(data): - """ - The SHA-256 primitive is abstracted here to allow platform- - aware hardware acceleration in the future. Currently only - uses Python's internal SHA-256 implementation. All SHA-256 - calls in RNS end up here. - """ digest = hashlib.sha256() digest.update(data) - return digest.digest() \ No newline at end of file + return digest.digest() + +def sha512(data): + digest = hashlib.sha512() + digest.update(data) + + return digest.digest() diff --git a/RNS/Cryptography/Proxies.py b/RNS/Cryptography/Proxies.py index 08b6959..5c0d7d7 100644 --- a/RNS/Cryptography/Proxies.py +++ b/RNS/Cryptography/Proxies.py @@ -2,6 +2,9 @@ from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey +# These proxy classes exist to create a uniform API accross +# cryptography primitive providers. + class X25519PrivateKeyProxy: def __init__(self, real): self.real = real @@ -40,4 +43,48 @@ class X25519PublicKeyProxy: return self.real.public_bytes( encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw - ) \ No newline at end of file + ) + + +class Ed25519PrivateKeyProxy: + def __init__(self, real): + self.real = real + + @classmethod + def generate(cls): + return cls(Ed25519PrivateKey.generate()) + + @classmethod + def from_private_bytes(cls, data): + return cls(Ed25519PrivateKey.from_private_bytes(data)) + + def private_bytes(self): + return self.real.private_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PrivateFormat.Raw, + encryption_algorithm=serialization.NoEncryption() + ) + + def public_key(self): + return Ed25519PublicKeyProxy(self.real.public_key()) + + def sign(self, message): + return self.real.sign(message) + + +class Ed25519PublicKeyProxy: + def __init__(self, real): + self.real = real + + @classmethod + def from_public_bytes(cls, data): + return cls(Ed25519PublicKey.from_public_bytes(data)) + + def public_bytes(self): + return self.real.public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw + ) + + def verify(self, signature, message): + self.real.verify(signature, message) \ No newline at end of file diff --git a/RNS/Cryptography/__init__.py b/RNS/Cryptography/__init__.py index 9401c8e..4ca7c4d 100644 --- a/RNS/Cryptography/__init__.py +++ b/RNS/Cryptography/__init__.py @@ -10,13 +10,13 @@ import RNS.Cryptography.Provider as cp if cp.PROVIDER == cp.PROVIDER_INTERNAL: from RNS.Cryptography.X25519 import X25519PrivateKey, X25519PublicKey - - # TODO: Use internal Ed25519 - from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey + from RNS.Cryptography.Ed25519 import Ed25519PrivateKey, Ed25519PublicKey elif cp.PROVIDER == cp.PROVIDER_PYCA: from RNS.Cryptography.Proxies import X25519PrivateKeyProxy as X25519PrivateKey from RNS.Cryptography.Proxies import X25519PublicKeyProxy as X25519PublicKey + from RNS.Cryptography.Proxies import Ed25519PrivateKeyProxy as Ed25519PrivateKey + from RNS.Cryptography.Proxies import Ed25519PublicKeyProxy as Ed25519PublicKey modules = glob.glob(os.path.dirname(__file__)+"/*.py") __all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')] diff --git a/RNS/Cryptography/pure25519/__init__.py b/RNS/Cryptography/pure25519/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/RNS/Cryptography/pure25519/_ed25519.py b/RNS/Cryptography/pure25519/_ed25519.py new file mode 100644 index 0000000..6b67569 --- /dev/null +++ b/RNS/Cryptography/pure25519/_ed25519.py @@ -0,0 +1,58 @@ +# MIT License +# +# Copyright (c) 2015 Brian Warner and other contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from . import eddsa + +class BadSignatureError(Exception): + pass + +SECRETKEYBYTES = 64 +PUBLICKEYBYTES = 32 +SIGNATUREKEYBYTES = 64 + +def publickey(seed32): + assert len(seed32) == 32 + vk32 = eddsa.publickey(seed32) + return vk32, seed32+vk32 + +def sign(msg, skvk): + assert len(skvk) == 64 + sk = skvk[:32] + vk = skvk[32:] + sig = eddsa.signature(msg, sk, vk) + return sig+msg + +def open(sigmsg, vk): + assert len(vk) == 32 + sig = sigmsg[:64] + msg = sigmsg[64:] + try: + valid = eddsa.checkvalid(sig, msg, vk) + except ValueError as e: + raise BadSignatureError(e) + except Exception as e: + if str(e) == "decoding point that is not on curve": + raise BadSignatureError(e) + raise + if not valid: + raise BadSignatureError() + return msg \ No newline at end of file diff --git a/RNS/Cryptography/pure25519/basic.py b/RNS/Cryptography/pure25519/basic.py new file mode 100644 index 0000000..e18c759 --- /dev/null +++ b/RNS/Cryptography/pure25519/basic.py @@ -0,0 +1,368 @@ +# MIT License +# +# Copyright (c) 2015 Brian Warner and other contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import binascii, hashlib, itertools + +Q = 2**255 - 19 +L = 2**252 + 27742317777372353535851937790883648493 + +def inv(x): + return pow(x, Q-2, Q) + +d = -121665 * inv(121666) +I = pow(2,(Q-1)//4,Q) + +def xrecover(y): + xx = (y*y-1) * inv(d*y*y+1) + x = pow(xx,(Q+3)//8,Q) + if (x*x - xx) % Q != 0: x = (x*I) % Q + if x % 2 != 0: x = Q-x + return x + +By = 4 * inv(5) +Bx = xrecover(By) +B = [Bx % Q,By % Q] + +# Extended Coordinates: x=X/Z, y=Y/Z, x*y=T/Z +# http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html + +def xform_affine_to_extended(pt): + (x, y) = pt + return (x%Q, y%Q, 1, (x*y)%Q) # (X,Y,Z,T) + +def xform_extended_to_affine(pt): + (x, y, z, _) = pt + return ((x*inv(z))%Q, (y*inv(z))%Q) + +def double_element(pt): # extended->extended + # dbl-2008-hwcd + (X1, Y1, Z1, _) = pt + A = (X1*X1) + B = (Y1*Y1) + C = (2*Z1*Z1) + D = (-A) % Q + J = (X1+Y1) % Q + E = (J*J-A-B) % Q + G = (D+B) % Q + F = (G-C) % Q + H = (D-B) % Q + X3 = (E*F) % Q + Y3 = (G*H) % Q + Z3 = (F*G) % Q + T3 = (E*H) % Q + return (X3, Y3, Z3, T3) + +def add_elements(pt1, pt2): # extended->extended + # add-2008-hwcd-3 . Slightly slower than add-2008-hwcd-4, but -3 is + # unified, so it's safe for general-purpose addition + (X1, Y1, Z1, T1) = pt1 + (X2, Y2, Z2, T2) = pt2 + A = ((Y1-X1)*(Y2-X2)) % Q + B = ((Y1+X1)*(Y2+X2)) % Q + C = T1*(2*d)*T2 % Q + D = Z1*2*Z2 % Q + E = (B-A) % Q + F = (D-C) % Q + G = (D+C) % Q + H = (B+A) % Q + X3 = (E*F) % Q + Y3 = (G*H) % Q + T3 = (E*H) % Q + Z3 = (F*G) % Q + return (X3, Y3, Z3, T3) + +def scalarmult_element_safe_slow(pt, n): + # this form is slightly slower, but tolerates arbitrary points, including + # those which are not in the main 1*L subgroup. This includes points of + # order 1 (the neutral element Zero), 2, 4, and 8. + assert n >= 0 + if n==0: + return xform_affine_to_extended((0,1)) + _ = double_element(scalarmult_element_safe_slow(pt, n>>1)) + return add_elements(_, pt) if n&1 else _ + +def _add_elements_nonunfied(pt1, pt2): # extended->extended + # add-2008-hwcd-4 : NOT unified, only for pt1!=pt2. About 10% faster than + # the (unified) add-2008-hwcd-3, and safe to use inside scalarmult if you + # aren't using points of order 1/2/4/8 + (X1, Y1, Z1, T1) = pt1 + (X2, Y2, Z2, T2) = pt2 + A = ((Y1-X1)*(Y2+X2)) % Q + B = ((Y1+X1)*(Y2-X2)) % Q + C = (Z1*2*T2) % Q + D = (T1*2*Z2) % Q + E = (D+C) % Q + F = (B-A) % Q + G = (B+A) % Q + H = (D-C) % Q + X3 = (E*F) % Q + Y3 = (G*H) % Q + Z3 = (F*G) % Q + T3 = (E*H) % Q + return (X3, Y3, Z3, T3) + +def scalarmult_element(pt, n): # extended->extended + # This form only works properly when given points that are a member of + # the main 1*L subgroup. It will give incorrect answers when called with + # the points of order 1/2/4/8, including point Zero. (it will also work + # properly when given points of order 2*L/4*L/8*L) + assert n >= 0 + if n==0: + return xform_affine_to_extended((0,1)) + _ = double_element(scalarmult_element(pt, n>>1)) + return _add_elements_nonunfied(_, pt) if n&1 else _ + +# points are encoded as 32-bytes little-endian, b255 is sign, b2b1b0 are 0 + +def encodepoint(P): + x = P[0] + y = P[1] + # MSB of output equals x.b0 (=x&1) + # rest of output is little-endian y + assert 0 <= y < (1<<255) # always < 0x7fff..ff + if x & 1: + y += 1<<255 + return binascii.unhexlify("%064x" % y)[::-1] + +def isoncurve(P): + x = P[0] + y = P[1] + return (-x*x + y*y - 1 - d*x*x*y*y) % Q == 0 + +class NotOnCurve(Exception): + pass + +def decodepoint(s): + unclamped = int(binascii.hexlify(s[:32][::-1]), 16) + clamp = (1 << 255) - 1 + y = unclamped & clamp # clear MSB + x = xrecover(y) + if bool(x & 1) != bool(unclamped & (1<<255)): x = Q-x + P = [x,y] + if not isoncurve(P): raise NotOnCurve("decoding point that is not on curve") + return P + +# scalars are encoded as 32-bytes little-endian + +def bytes_to_scalar(s): + assert len(s) == 32, len(s) + return int(binascii.hexlify(s[::-1]), 16) + +def bytes_to_clamped_scalar(s): + # Ed25519 private keys clamp the scalar to ensure two things: + # 1: integer value is in L/2 .. L, to avoid small-logarithm + # non-wraparaound + # 2: low-order 3 bits are zero, so a small-subgroup attack won't learn + # any information + # set the top two bits to 01, and the bottom three to 000 + a_unclamped = bytes_to_scalar(s) + AND_CLAMP = (1<<254) - 1 - 7 + OR_CLAMP = (1<<254) + a_clamped = (a_unclamped & AND_CLAMP) | OR_CLAMP + return a_clamped + +def random_scalar(entropy_f): # 0..L-1 inclusive + # reduce the bias to a safe level by generating 256 extra bits + oversized = int(binascii.hexlify(entropy_f(32+32)), 16) + return oversized % L + +def password_to_scalar(pw): + oversized = hashlib.sha512(pw).digest() + return int(binascii.hexlify(oversized), 16) % L + +def scalar_to_bytes(y): + y = y % L + assert 0 <= y < 2**256 + return binascii.unhexlify("%064x" % y)[::-1] + +# Elements, of various orders + +def is_extended_zero(XYTZ): + # catch Zero + (X, Y, Z, T) = XYTZ + Y = Y % Q + Z = Z % Q + if X==0 and Y==Z and Y!=0: + return True + return False + +class ElementOfUnknownGroup: + # This is used for points of order 2,4,8,2*L,4*L,8*L + def __init__(self, XYTZ): + assert isinstance(XYTZ, tuple) + assert len(XYTZ) == 4 + self.XYTZ = XYTZ + + def add(self, other): + if not isinstance(other, ElementOfUnknownGroup): + raise TypeError("elements can only be added to other elements") + sum_XYTZ = add_elements(self.XYTZ, other.XYTZ) + if is_extended_zero(sum_XYTZ): + return Zero + return ElementOfUnknownGroup(sum_XYTZ) + + def scalarmult(self, s): + if isinstance(s, ElementOfUnknownGroup): + raise TypeError("elements cannot be multiplied together") + assert s >= 0 + product = scalarmult_element_safe_slow(self.XYTZ, s) + return ElementOfUnknownGroup(product) + + def to_bytes(self): + return encodepoint(xform_extended_to_affine(self.XYTZ)) + def __eq__(self, other): + return self.to_bytes() == other.to_bytes() + def __ne__(self, other): + return not self == other + +class Element(ElementOfUnknownGroup): + # this only holds elements in the main 1*L subgroup. It never holds Zero, + # or elements of order 1/2/4/8, or 2*L/4*L/8*L. + + def add(self, other): + if not isinstance(other, ElementOfUnknownGroup): + raise TypeError("elements can only be added to other elements") + sum_element = ElementOfUnknownGroup.add(self, other) + if sum_element is Zero: + return sum_element + if isinstance(other, Element): + # adding two subgroup elements results in another subgroup + # element, or Zero, and we've already excluded Zero + return Element(sum_element.XYTZ) + # not necessarily a subgroup member, so assume not + return sum_element + + def scalarmult(self, s): + if isinstance(s, ElementOfUnknownGroup): + raise TypeError("elements cannot be multiplied together") + # scalarmult of subgroup members can be done modulo the subgroup + # order, and using the faster non-unified function. + s = s % L + # scalarmult(s=0) gets you Zero + if s == 0: + return Zero + # scalarmult(s=1) gets you self, which is a subgroup member + # scalarmult(s