From fd2166011600c753f3ff103078f711302d7887ab Mon Sep 17 00:00:00 2001 From: Niko Date: Wed, 3 Jun 2026 21:09:45 +0200 Subject: [PATCH] fix: correct URLs, add sha512, updated ed25519 and identity_iot --- .../identity_iot_ed25519.cpython-312.pyc | Bin 0 -> 7433 bytes config.json | 15 ++ identity_iot.py | 195 +++++++++-------- identity_iot_ed25519.py | 205 +++++++----------- sha512.py | 87 ++++++++ 5 files changed, 286 insertions(+), 216 deletions(-) create mode 100644 __pycache__/identity_iot_ed25519.cpython-312.pyc create mode 100644 config.json create mode 100644 sha512.py diff --git a/__pycache__/identity_iot_ed25519.cpython-312.pyc b/__pycache__/identity_iot_ed25519.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9619549617d02b8ffabd50adb7121ee946ae73e7 GIT binary patch literal 7433 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 Y^~8@o@W38;-1XDPr)~d= exp except Exception: - return True # assume expired on any error + return True # ========================================================================= - # HTTP transport — ITEMS envelope (AES-CBC) or plain JSON + # WiFi reconnect + # ========================================================================= + + def _ensure_wifi(self): + wlan = network.WLAN(network.STA_IF) + if wlan.isconnected(): + return True + if not self._wifi_ssid: + return False + print('WiFi lost, reconnecting...') + wlan.active(True) + wlan.connect(self._wifi_ssid, self._wifi_pass) + timeout = 15 + while not wlan.isconnected() and timeout > 0: + utime.sleep(1) + timeout -= 1 + return wlan.isconnected() + + # ========================================================================= + # HTTP transport # ========================================================================= def _post(self, inner: dict) -> dict: - """ - POST to backend. - If comm_key/iv provided: ITEMS AES-CBC envelope. - Otherwise: plain JSON (for PoC / testing). - """ if self._comm_key and self._comm_iv: return self._post_encrypted(inner) return self._post_plain(inner) def _post_plain(self, inner: dict) -> dict: - """Plain JSON POST — for PoC and testing.""" - body = ujson.dumps({'data': ujson.dumps(inner)}) - resp = requests.post( - self.base_url, - headers={'Content-Type': 'application/json'}, - data=body, - ) - outer = ujson.loads(resp.text) - resp.close() - # Backend returns encrypted data — decode if possible, else return raw - if 'data' in outer: + for attempt in range(3): try: - return ujson.loads(outer['data']) - except Exception: + if not self._ensure_wifi(): + raise Exception('WiFi not connected') + body = ujson.dumps({'data': ujson.dumps(inner)}) + resp = requests.post( + self.base_url, + headers={'Content-Type': 'application/json'}, + data=body, + timeout=20 + ) + outer = ujson.loads(resp.text) + resp.close() + if 'data' in outer: + try: + return ujson.loads(outer['data']) + except Exception: + return outer return outer - return outer + except Exception as e: + print('plain retry', attempt + 1, ':', e) + utime.sleep(3) + raise Exception('POST failed after 3 attempts') def _post_encrypted(self, inner: dict) -> dict: - """ITEMS AES-CBC envelope POST.""" from identity_iot_aes import aes_cbc_encrypt, aes_cbc_decrypt - inner_json = ujson.dumps(inner) - cipher_b64 = aes_cbc_encrypt(inner_json, self._comm_key, self._comm_iv) - body = ujson.dumps({'data': cipher_b64}) - resp = requests.post( - self.base_url, - headers={'Content-Type': 'application/json'}, - data=body, - ) - outer = ujson.loads(resp.text) - resp.close() - if 'data' not in outer: - raise Exception('Missing outer.data in response') - clear = aes_cbc_decrypt(outer['data'], self._comm_key, self._comm_iv) - return ujson.loads(clear) + for attempt in range(3): + try: + if not self._ensure_wifi(): + raise Exception('WiFi not connected') + inner_json = ujson.dumps(inner) + cipher_b64 = aes_cbc_encrypt(inner_json, self._comm_key, self._comm_iv) + body = ujson.dumps({'data': cipher_b64}) + resp = requests.post( + self.base_url, + headers={'Content-Type': 'application/json'}, + data=body, + timeout=20 + ) + print('status:', resp.status_code) + print('raw:', resp.text[:100]) + outer = ujson.loads(resp.text) + resp.close() + if 'data' not in outer: + raise Exception('Missing outer.data') + clear = aes_cbc_decrypt(outer['data'], self._comm_key, self._comm_iv) + return ujson.loads(clear) + except Exception as e: + print('enc retry', attempt + 1, ':', e) + utime.sleep(3) + raise Exception('POST failed after 3 attempts') # ============================================================================= @@ -279,7 +285,6 @@ class IdentityIoT: # ============================================================================= def _normalize_b64(s: str) -> str: - """Normalize base64 string for ubinascii.""" s = s.strip().replace('\n', '').replace('\r', '').replace(' ', '') s = s.replace('-', '+').replace('_', '/') pad = len(s) % 4 diff --git a/identity_iot_ed25519.py b/identity_iot_ed25519.py index cffc935..721097f 100644 --- a/identity_iot_ed25519.py +++ b/identity_iot_ed25519.py @@ -1,163 +1,126 @@ # ============================================================+ -# Identity IoT — Ed25519 Pure Python +# Identity IoT — Ed25519 for MicroPython # (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. +# Based on Bernstein et al. reference implementation (public domain) +# Iterative scalarmult — no recursion, safe for MicroPython stack # ============================================================+ -import uhashlib -import ubinascii +from sha512 import sha512 -# ---- 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) +_q = 2**255 - 19 +_l = 2**252 + 27742317777372353535851937790883648493 +_d = -121665 * pow(121666, _q - 2, _q) % _q +_I = pow(2, (_q - 1) // 4, _q) -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 _xrecover(y): + x2 = (y * y - 1) * pow(_d * y * y + 1, _q - 2, _q) + x = pow(x2, (_q + 3) // 8, _q) + if (x * x - x2) % _q != 0: + x = x * _I % _q + if x % 2 != 0: + x = _q - x + return x -def _clamp(h: bytes) -> int: - a = bytearray(h[:32]) - a[0] &= 248 - a[31] &= 127 - a[31] |= 64 - return int.from_bytes(a, 'little') +_By = 4 * pow(5, _q - 2, _q) % _q +_Bx = _xrecover(_By) +_B = [_Bx % _q, _By % _q, 1, _Bx * _By % _q] -def _point_add(P, Q): +def _edwards_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) + 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; F = D - C; G = D + C; H = B + A + return [E * F % _q, G * H % _q, F * G % _q, E * H % _q] -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 +def _scalarmult(P, e): + # Iterative double-and-add — no recursion + Q = [0, 1, 1, 0] # neutral element + while e > 0: + if e & 1: + Q = _edwards_add(Q, P) + P = _edwards_add(P, P) + e >>= 1 return Q -def _point_encode(P) -> bytes: +def _encodeint(y): + bits = [(y >> i) & 1 for i in range(256)] + return bytes([sum([bits[i * 8 + j] << j for j in range(8)]) for i in range(32)]) + + +def _encodepoint(P): 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) + zi = pow(z, _q - 2, _q) + x = x * zi % _q + y = y * zi % _q + bits = [(y >> i) & 1 for i in range(255)] + [x & 1] + return bytes([sum([bits[i * 8 + j] << j for j in range(8)]) for i in range(32)]) -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 _bit(h, i): + return (h[i // 8] >> (i % 8)) & 1 +def _hint(m): + return int.from_bytes(sha512(m), 'little') + + +def _decodepoint(s): + y = sum(_bit(s, i) * 2 ** i for i in range(255)) + x = _xrecover(y) + if x & 1 != _bit(s, 255): + x = _q - x + return [x, y, 1, x * y % _q] + + +# ============================================================ +# Public API +# ============================================================ + 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 + h = sha512(seed) + a = 2 ** 254 + sum(2 ** i * _bit(h, i) for i in range(3, 254)) + A = _scalarmult(_B, a) + pk = _encodepoint(A) + return pk, seed + pk 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 + h = sha512(seed) + a = 2 ** 254 + sum(2 ** i * _bit(h, i) for i in range(3, 254)) + pk = ed25519_keypair_from_seed(seed)[0] + r = _hint(h[32:64] + message) + R = _scalarmult(_B, r) + S = (r + _hint(_encodepoint(R) + pk + message) * a) % _l + return _encodepoint(R) + _encodeint(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: +def ed25519_verify(pk: bytes, message: bytes, signature: bytes) -> bool: + if len(signature) != 64 or len(pk) != 32: return False try: R_enc = signature[:32] S = int.from_bytes(signature[32:], 'little') - if S >= _q: + if S >= _l: 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) + A = _decodepoint(pk) + R = _decodepoint(R_enc) + h = _hint(R_enc + pk + message) + lhs = _scalarmult(_B, 8 * S) + rhs = _edwards_add(_scalarmult(R, 8), _scalarmult(A, 8 * h)) + return _encodepoint(lhs) == _encodepoint(rhs) except Exception: return False diff --git a/sha512.py b/sha512.py new file mode 100644 index 0000000..4e46143 --- /dev/null +++ b/sha512.py @@ -0,0 +1,87 @@ +# SHA-512 pure Python for MicroPython +# Based on public domain implementation +# Compact version optimized for MicroPython memory constraints + +_K = [ + 0x428a2f98d728ae22, 0x7137449123ef65cd, 0xb5c0fbcfec4d3b2f, 0xe9b5dba58189dbbc, + 0x3956c25bf348b538, 0x59f111f1b605d019, 0x923f82a4af194f9b, 0xab1c5ed5da6d8118, + 0xd807aa98a3030242, 0x12835b0145706fbe, 0x243185be4ee4b28c, 0x550c7dc3d5ffb4e2, + 0x72be5d74f27b896f, 0x80deb1fe3b1696b1, 0x9bdc06a725c71235, 0xc19bf174cf692694, + 0xe49b69c19ef14ad2, 0xefbe4786384f25e3, 0x0fc19dc68b8cd5b5, 0x240ca1cc77ac9c65, + 0x2de92c6f592b0275, 0x4a7484aa6ea6e483, 0x5cb0a9dcbd41fbd4, 0x76f988da831153b5, + 0x983e5152ee66dfab, 0xa831c66d2db43210, 0xb00327c898fb213f, 0xbf597fc7beef0ee4, + 0xc6e00bf33da88fc2, 0xd5a79147930aa725, 0x06ca6351e003826f, 0x142929670a0e6e70, + 0x27b70a8546d22ffc, 0x2e1b21385c26c926, 0x4d2c6dfc5ac42aed, 0x53380d139d95b3df, + 0x650a73548baf63de, 0x766a0abb3c77b2a8, 0x81c2c92e47edaee6, 0x92722c851482353b, + 0xa2bfe8a14cf10364, 0xa81a664bbc423001, 0xc24b8b70d0f89791, 0xc76c51a30654be30, + 0xd192e819d6ef5218, 0xd69906245565a910, 0xf40e35855771202a, 0x106aa07032bbd1b8, + 0x19a4c116b8d2d0c8, 0x1e376c085141ab53, 0x2748774cdf8eeb99, 0x34b0bcb5e19b48a8, + 0x391c0cb3c5c95a63, 0x4ed8aa4ae3418acb, 0x5b9cca4f7763e373, 0x682e6ff3d6b2b8a3, + 0x748f82ee5defb2fc, 0x78a5636f43172f60, 0x84c87814a1f0ab72, 0x8cc702081a6439ec, + 0x90befffa23631e28, 0xa4506cebde82bde9, 0xbef9a3f7b2c67915, 0xc67178f2e372532b, + 0xca273eceea26619c, 0xd186b8c721c0c207, 0xeada7dd6cde0eb1e, 0xf57d4f7fee6ed178, + 0x06f067aa72176fba, 0x0a637dc5a2c898a6, 0x113f9804bef90dae, 0x1b710b35131c471b, + 0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc, 0x431d67c49c100d4c, + 0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817, +] + +_M64 = 0xffffffffffffffff + +def _rotr64(x, n): + return ((x >> n) | (x << (64 - n))) & _M64 + +def _sha512_compress(state, block): + w = list(block) + for i in range(16, 80): + s0 = _rotr64(w[i-15], 1) ^ _rotr64(w[i-15], 8) ^ (w[i-15] >> 7) + s1 = _rotr64(w[i-2], 19) ^ _rotr64(w[i-2], 61) ^ (w[i-2] >> 6) + w.append((w[i-16] + s0 + w[i-7] + s1) & _M64) + + a, b, c, d, e, f, g, h = state + + for i in range(80): + S1 = _rotr64(e, 14) ^ _rotr64(e, 18) ^ _rotr64(e, 41) + ch = (e & f) ^ ((~e & _M64) & g) + temp1 = (h + S1 + ch + _K[i] + w[i]) & _M64 + S0 = _rotr64(a, 28) ^ _rotr64(a, 34) ^ _rotr64(a, 39) + maj = (a & b) ^ (a & c) ^ (b & c) + temp2 = (S0 + maj) & _M64 + + h = g; g = f; f = e + e = (d + temp1) & _M64 + d = c; c = b; b = a + a = (temp1 + temp2) & _M64 + + return [ + (state[0] + a) & _M64, (state[1] + b) & _M64, + (state[2] + c) & _M64, (state[3] + d) & _M64, + (state[4] + e) & _M64, (state[5] + f) & _M64, + (state[6] + g) & _M64, (state[7] + h) & _M64, + ] + +def sha512(data: bytes) -> bytes: + # Initial hash values + state = [ + 0x6a09e667f3bcc908, 0xbb67ae8584caa73b, + 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, 0x9b05688c2b3e6c1f, + 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179, + ] + + msg = bytearray(data) + length = len(data) * 8 + msg.append(0x80) + while len(msg) % 128 != 112: + msg.append(0x00) + # Append length as 128-bit big-endian (high 64 bits are 0) + msg += (0).to_bytes(8, 'big') + msg += length.to_bytes(8, 'big') + + for i in range(0, len(msg), 128): + block = [] + for j in range(0, 128, 8): + block.append(int.from_bytes(msg[i+j:i+j+8], 'big')) + state = _sha512_compress(state, block) + + return b''.join(s.to_bytes(8, 'big') for s in state) +