feat: identity-micropython v0.1.0 — Pico W Ed25519 PoC

This commit is contained in:
2026-05-21 23:07:50 +02:00
commit d23062d54f
7 changed files with 716 additions and 0 deletions

163
identity_iot_ed25519.py Normal file
View File

@@ -0,0 +1,163 @@
# ============================================================+
# 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