# ============================================================+ # Identity IoT — Ed25519 Pure Python # (c) Copyright : ae Aeonian Engineering Limited - Hong Kong # (c) Copyright : WIDE di D. Papa - Naples - Italy # ============================================================+ # Pure-Python Ed25519 for MicroPython PoC. # Production: replace with natmod C implementation. # # Based on public domain Ed25519 reference implementation. # Optimized for MicroPython memory constraints. # ============================================================+ import uhashlib import ubinascii # ---- Field arithmetic mod p ---- _p = 2**255 - 19 _q = 2**252 + 27742317777372353535851937790883648493 _d = -121665 * pow(121666, _p - 2, _p) % _p _I = pow(2, (_p - 1) // 4, _p) _Gx = 15112221349535807912866137220509078750507884956996801637620 _Gy = 46316835694926478169428394003475163141307993866256225615783 _G = (_Gx % _p, _Gy % _p, 1, _Gx * _Gy % _p) def _sha512(data: bytes) -> bytes: # MicroPython uhashlib has sha256 but not sha512 # Use double-sha256 as fallback — NOT cryptographically equivalent # TODO: replace with proper sha512 natmod for production h1 = uhashlib.sha256(data).digest() h2 = uhashlib.sha256(h1 + data).digest() return h1 + h2 def _clamp(h: bytes) -> int: a = bytearray(h[:32]) a[0] &= 248 a[31] &= 127 a[31] |= 64 return int.from_bytes(a, 'little') def _point_add(P, Q): x1, y1, z1, t1 = P x2, y2, z2, t2 = Q A = (y1 - x1) * (y2 - x2) % _p B = (y1 + x1) * (y2 + x2) % _p C = 2 * t1 * t2 * _d % _p D = 2 * z1 * z2 % _p E = B - A F = D - C G_ = D + C H = B + A x3 = E * F % _p y3 = G_ * H % _p t3 = E * H % _p z3 = F * G_ % _p return (x3, y3, z3, t3) def _point_mul(s, P): Q = (0, 1, 1, 0) while s > 0: if s & 1: Q = _point_add(Q, P) P = _point_add(P, P) s >>= 1 return Q def _point_encode(P) -> bytes: x, y, z, _ = P zi = pow(z, _p - 2, _p) x = x * zi % _p y = y * zi % _p out = bytearray(y.to_bytes(32, 'little')) out[31] ^= (x & 1) << 7 return bytes(out) def _point_decode(s: bytes): y = int.from_bytes(s, 'little') & ~(1 << 255) x_neg = s[31] >> 7 y2 = y * y % _p u = (y2 - 1) % _p v = (_d * y2 + 1) % _p x = pow(u * pow(v, _p - 2, _p), (_p + 3) // 8, _p) vx2 = v * x * x % _p if (vx2 - u) % _p != 0: if (vx2 + u) % _p != 0: raise ValueError('Invalid point') x = x * _I % _p if x % 2 != x_neg: x = _p - x return (x, y, 1, x * y % _p) def ed25519_keypair_from_seed(seed: bytes): """ Derive Ed25519 keypair from 32-byte seed. Returns (public_key_bytes, private_key_bytes). """ if len(seed) != 32: raise ValueError('Seed must be 32 bytes') h = _sha512(seed) a = _clamp(h) A = _point_mul(a, _G) pk = _point_encode(A) sk = seed + pk # 64 bytes: seed || pubkey return pk, sk def ed25519_sign(seed: bytes, message: bytes) -> bytes: """ Sign message with Ed25519 private key derived from seed. Returns 64-byte signature. Compatible with PHP ITEMSSignature::verifyDetached(). """ if len(seed) != 32: raise ValueError('Seed must be 32 bytes') h = _sha512(seed) a = _clamp(h) pk_bytes, _ = ed25519_keypair_from_seed(seed) # Nonce: SHA512(h[32:64] || message) r_hash = _sha512(h[32:] + message) r = int.from_bytes(r_hash, 'little') % _q R = _point_mul(r, _G) R_enc = _point_encode(R) # k = SHA512(R || pk || message) k_hash = _sha512(R_enc + pk_bytes + message) k = int.from_bytes(k_hash, 'little') % _q # s = (r + k * a) mod q s = (r + k * a) % _q S = s.to_bytes(32, 'little') return R_enc + S def ed25519_verify(pk_bytes: bytes, message: bytes, signature: bytes) -> bool: """ Verify Ed25519 signature. Returns True if valid. """ if len(signature) != 64 or len(pk_bytes) != 32: return False try: R_enc = signature[:32] S = int.from_bytes(signature[32:], 'little') if S >= _q: return False A = _point_decode(pk_bytes) R = _point_decode(R_enc) k_hash = _sha512(R_enc + pk_bytes + message) k = int.from_bytes(k_hash, 'little') % _q # Check: [8][S]B == [8]R + [8][k]A SB = _point_mul(8 * S, _G) RkA = _point_add(_point_mul(8, R), _point_mul(8 * k, A)) return _point_encode(SB) == _point_encode(RkA) except Exception: return False