mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-04-20 15:52:35 -05:00
feat: add sect233r1 ECDSA verification for 3DS OTP cert
pure python GF(2^233) field arithmetic, binary curve point operations, and ECDSA-SHA256 on sect233r1. verifies OTP CTCert against nintendo root CA public key. zero dependencies. sign+verify round-trip tested, n*G=O verified, wrong key/message rejection confirmed.
This commit is contained in:
@@ -295,17 +295,27 @@ def verify_otp(
|
|||||||
filepath: str | Path,
|
filepath: str | Path,
|
||||||
keys: dict[str, dict[str, bytes]],
|
keys: dict[str, dict[str, bytes]],
|
||||||
) -> tuple[bool, str]:
|
) -> tuple[bool, str]:
|
||||||
"""Verify otp.bin: AES-128-CBC decrypt + magic + SHA-256 hash.
|
"""Verify otp.bin: AES-128-CBC decrypt + magic + SHA-256 hash + ECC cert.
|
||||||
|
|
||||||
Source: Azahar src/core/file_sys/otp.cpp
|
Source: Azahar src/core/file_sys/otp.cpp, src/core/hw/unique_data.cpp
|
||||||
Struct: 0xE0 body + 0x20 SHA256 hash = 0x100
|
Struct: 0xE0 body + 0x20 SHA256 hash = 0x100
|
||||||
|
|
||||||
ECC certificate verification (sect233r1) is not reproduced — requires
|
OTP body layout:
|
||||||
binary field curve arithmetic unavailable in standard Python libraries.
|
0x000: u32 magic (0xDEADB00F)
|
||||||
AES decryption + SHA-256 hash is sufficient to prove file integrity.
|
0x004: u32 device_id
|
||||||
|
0x018: u8 otp_version
|
||||||
|
0x019: u8 system_type (0=retail, non-zero=dev)
|
||||||
|
0x020: u32 ctcert.expiry_date
|
||||||
|
0x024: 0x20 ctcert.priv_key (ECC private key)
|
||||||
|
0x044: 0x3C ctcert.signature (ECC signature r||s)
|
||||||
|
|
||||||
|
Certificate body (what is signed): issuer(0x40) + key_type(4) + name(0x40)
|
||||||
|
+ expiration(4) + public_key_xy(0x3C), aligned to 0x100.
|
||||||
|
|
||||||
Returns (valid, reason_string).
|
Returns (valid, reason_string).
|
||||||
"""
|
"""
|
||||||
|
from sect233r1 import ecdsa_verify_sha256, _ec_mul, _Gx, _Gy, _N
|
||||||
|
|
||||||
data = bytearray(Path(filepath).read_bytes())
|
data = bytearray(Path(filepath).read_bytes())
|
||||||
|
|
||||||
if len(data) != 0x100:
|
if len(data) != 0x100:
|
||||||
@@ -336,7 +346,71 @@ def verify_otp(
|
|||||||
if computed_hash != stored_hash:
|
if computed_hash != stored_hash:
|
||||||
return False, "SHA-256 hash mismatch (OTP corrupted)"
|
return False, "SHA-256 hash mismatch (OTP corrupted)"
|
||||||
|
|
||||||
return True, "decrypted, magic valid, SHA-256 valid (ECC cert not verified — sect233r1)"
|
# --- ECC certificate verification (sect233r1) ---
|
||||||
|
ecc_keys = keys.get("ECC", {})
|
||||||
|
root_public_xy = ecc_keys.get("rootPublicXY")
|
||||||
|
if not root_public_xy or len(root_public_xy) != 60:
|
||||||
|
return True, "decrypted, magic valid, SHA-256 valid (ECC skipped: no rootPublicXY)"
|
||||||
|
|
||||||
|
# Extract CTCert fields from OTP body
|
||||||
|
device_id = struct.unpack_from("<I", data, 0x04)[0]
|
||||||
|
otp_version = data[0x18]
|
||||||
|
system_type = data[0x19]
|
||||||
|
|
||||||
|
# Expiry date endianness depends on otp_version
|
||||||
|
if otp_version < 5:
|
||||||
|
expiry_date = struct.unpack_from(">I", data, 0x20)[0]
|
||||||
|
else:
|
||||||
|
expiry_date = struct.unpack_from("<I", data, 0x20)[0]
|
||||||
|
|
||||||
|
# ECC private key (0x20 bytes at offset 0x24)
|
||||||
|
priv_key_raw = bytes(data[0x24:0x44])
|
||||||
|
# ECC signature (0x3C bytes at offset 0x44)
|
||||||
|
signature_rs = bytes(data[0x44:0x80])
|
||||||
|
|
||||||
|
# Fix up private key: privkey % subgroup_order (Nintendo's ECC lib quirk)
|
||||||
|
priv_key_int = int.from_bytes(priv_key_raw, "big") % _N
|
||||||
|
|
||||||
|
# Derive public key from private key: Q = privkey * G
|
||||||
|
pub_point = _ec_mul(priv_key_int, (_Gx, _Gy))
|
||||||
|
if pub_point is None:
|
||||||
|
return False, "ECC cert: derived public key is point at infinity"
|
||||||
|
pub_key_xy = (
|
||||||
|
pub_point[0].to_bytes(30, "big") + pub_point[1].to_bytes(30, "big")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build certificate body (what was signed)
|
||||||
|
# Issuer: "Nintendo CA - G3_NintendoCTR2prod" or "...dev"
|
||||||
|
issuer = bytearray(0x40)
|
||||||
|
if system_type == 0:
|
||||||
|
issuer_str = b"Nintendo CA - G3_NintendoCTR2prod"
|
||||||
|
else:
|
||||||
|
issuer_str = b"Nintendo CA - G3_NintendoCTR2dev"
|
||||||
|
issuer[:len(issuer_str)] = issuer_str
|
||||||
|
|
||||||
|
# Name: "CT{device_id:08X}-{system_type:02X}"
|
||||||
|
name = bytearray(0x40)
|
||||||
|
name_str = f"CT{device_id:08X}-{system_type:02X}".encode()
|
||||||
|
name[:len(name_str)] = name_str
|
||||||
|
|
||||||
|
# Key type = 2 (ECC), big-endian u32
|
||||||
|
key_type = struct.pack(">I", 2)
|
||||||
|
|
||||||
|
# Expiry date as big-endian u32
|
||||||
|
expiry_be = struct.pack(">I", expiry_date)
|
||||||
|
|
||||||
|
# Serialize: issuer + key_type + name + expiry + public_key_xy
|
||||||
|
cert_body = bytes(issuer) + key_type + bytes(name) + expiry_be + pub_key_xy
|
||||||
|
# Align up to 0x40
|
||||||
|
aligned_size = ((len(cert_body) + 0x3F) // 0x40) * 0x40
|
||||||
|
cert_body_padded = cert_body.ljust(aligned_size, b"\x00")
|
||||||
|
|
||||||
|
# Verify ECDSA-SHA256 signature against root public key
|
||||||
|
valid = ecdsa_verify_sha256(cert_body_padded, signature_rs, root_public_xy)
|
||||||
|
|
||||||
|
if valid:
|
||||||
|
return True, "decrypted, magic valid, SHA-256 valid, ECC cert valid"
|
||||||
|
return False, "decrypted, magic+SHA256 valid, but ECC cert signature invalid"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
"""Pure Python ECDSA verification on sect233r1 (binary field curve).
|
||||||
|
|
||||||
|
Implements GF(2^233) field arithmetic, elliptic curve point operations,
|
||||||
|
and ECDSA-SHA256 verification for Nintendo 3DS OTP certificate checking.
|
||||||
|
|
||||||
|
Zero external dependencies — uses only Python stdlib.
|
||||||
|
|
||||||
|
Curve: sect233r1 (NIST B-233, SEC 2 v2)
|
||||||
|
Field: GF(2^233) with irreducible polynomial t^233 + t^74 + 1
|
||||||
|
Equation: y^2 + xy = x^3 + x^2 + b
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# sect233r1 curve parameters (SEC 2 v2)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_M = 233
|
||||||
|
_F = (1 << 233) | (1 << 74) | 1 # irreducible polynomial
|
||||||
|
|
||||||
|
_A = 1
|
||||||
|
_B = 0x0066647EDE6C332C7F8C0923BB58213B333B20E9CE4281FE115F7D8F90AD
|
||||||
|
|
||||||
|
_Gx = 0x00FAC9DFCBAC8313BB2139F1BB755FEF65BC391F8B36F8F8EB7371FD558B
|
||||||
|
_Gy = 0x01006A08A41903350678E58528BEBF8A0BEFF867A7CA36716F7E01F81052
|
||||||
|
|
||||||
|
# Subgroup order
|
||||||
|
_N = 0x01000000000000000000000000000013E974E72F8A6922031D2603CFE0D7
|
||||||
|
_N_BITLEN = _N.bit_length() # 233
|
||||||
|
|
||||||
|
# Cofactor
|
||||||
|
_H = 2
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# GF(2^233) field arithmetic
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _gf_reduce(a: int) -> int:
|
||||||
|
"""Reduce polynomial a modulo t^233 + t^74 + 1."""
|
||||||
|
while a.bit_length() > _M:
|
||||||
|
shift = a.bit_length() - 1 - _M
|
||||||
|
a ^= _F << shift
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
|
def _gf_add(a: int, b: int) -> int:
|
||||||
|
"""Add two elements in GF(2^233). Addition = XOR."""
|
||||||
|
return a ^ b
|
||||||
|
|
||||||
|
|
||||||
|
def _gf_mul(a: int, b: int) -> int:
|
||||||
|
"""Multiply two elements in GF(2^233)."""
|
||||||
|
a = _gf_reduce(a)
|
||||||
|
result = 0
|
||||||
|
while b:
|
||||||
|
if b & 1:
|
||||||
|
result ^= a
|
||||||
|
a <<= 1
|
||||||
|
b >>= 1
|
||||||
|
return _gf_reduce(result)
|
||||||
|
|
||||||
|
|
||||||
|
def _gf_sqr(a: int) -> int:
|
||||||
|
"""Square an element in GF(2^233)."""
|
||||||
|
return _gf_mul(a, a)
|
||||||
|
|
||||||
|
|
||||||
|
def _gf_inv(a: int) -> int:
|
||||||
|
"""Multiplicative inverse in GF(2^233) using extended Euclidean algorithm."""
|
||||||
|
if a == 0:
|
||||||
|
raise ZeroDivisionError("inverse of zero in GF(2^m)")
|
||||||
|
# Extended GCD for polynomials in GF(2)[x]
|
||||||
|
old_r, r = _F, a
|
||||||
|
old_s, s = 0, 1
|
||||||
|
while r != 0:
|
||||||
|
# Polynomial division: old_r = q * r + remainder
|
||||||
|
q = 0
|
||||||
|
temp = old_r
|
||||||
|
dr = r.bit_length() - 1
|
||||||
|
while temp != 0 and temp.bit_length() - 1 >= dr:
|
||||||
|
shift = temp.bit_length() - 1 - dr
|
||||||
|
q ^= 1 << shift
|
||||||
|
temp ^= r << shift
|
||||||
|
remainder = temp
|
||||||
|
# Multiply q * s in GF(2)[x] (no reduction — working in polynomial ring)
|
||||||
|
qs = 0
|
||||||
|
qt = q
|
||||||
|
st = s
|
||||||
|
while qt:
|
||||||
|
if qt & 1:
|
||||||
|
qs ^= st
|
||||||
|
st <<= 1
|
||||||
|
qt >>= 1
|
||||||
|
old_r, r = r, remainder
|
||||||
|
old_s, s = s, old_s ^ qs
|
||||||
|
# old_r should be 1 (the GCD)
|
||||||
|
if old_r != 1:
|
||||||
|
raise ValueError("element not invertible")
|
||||||
|
return _gf_reduce(old_s)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Elliptic curve point operations on sect233r1
|
||||||
|
# y^2 + xy = x^3 + ax^2 + b (a=1)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Point at infinity
|
||||||
|
_INF = None
|
||||||
|
|
||||||
|
|
||||||
|
def _ec_add(
|
||||||
|
p: tuple[int, int] | None,
|
||||||
|
q: tuple[int, int] | None,
|
||||||
|
) -> tuple[int, int] | None:
|
||||||
|
"""Add two points on the curve."""
|
||||||
|
if p is _INF:
|
||||||
|
return q
|
||||||
|
if q is _INF:
|
||||||
|
return p
|
||||||
|
|
||||||
|
x1, y1 = p
|
||||||
|
x2, y2 = q
|
||||||
|
|
||||||
|
if x1 == x2:
|
||||||
|
if y1 == _gf_add(y2, x2):
|
||||||
|
# P + (-P) = O
|
||||||
|
return _INF
|
||||||
|
if y1 == y2:
|
||||||
|
# P == Q, use doubling
|
||||||
|
return _ec_double(p)
|
||||||
|
return _INF
|
||||||
|
|
||||||
|
# lambda = (y1 + y2) / (x1 + x2)
|
||||||
|
lam = _gf_mul(_gf_add(y1, y2), _gf_inv(_gf_add(x1, x2)))
|
||||||
|
# x3 = lambda^2 + lambda + x1 + x2 + a
|
||||||
|
x3 = _gf_add(_gf_add(_gf_add(_gf_sqr(lam), lam), _gf_add(x1, x2)), _A)
|
||||||
|
# y3 = lambda * (x1 + x3) + x3 + y1
|
||||||
|
y3 = _gf_add(_gf_add(_gf_mul(lam, _gf_add(x1, x3)), x3), y1)
|
||||||
|
return (x3, y3)
|
||||||
|
|
||||||
|
|
||||||
|
def _ec_double(p: tuple[int, int] | None) -> tuple[int, int] | None:
|
||||||
|
"""Double a point on the curve."""
|
||||||
|
if p is _INF:
|
||||||
|
return _INF
|
||||||
|
|
||||||
|
x1, y1 = p
|
||||||
|
if x1 == 0:
|
||||||
|
return _INF
|
||||||
|
|
||||||
|
# lambda = x1 + y1/x1
|
||||||
|
lam = _gf_add(x1, _gf_mul(y1, _gf_inv(x1)))
|
||||||
|
# x3 = lambda^2 + lambda + a
|
||||||
|
x3 = _gf_add(_gf_add(_gf_sqr(lam), lam), _A)
|
||||||
|
# y3 = x1^2 + (lambda + 1) * x3
|
||||||
|
y3 = _gf_add(_gf_sqr(x1), _gf_mul(_gf_add(lam, 1), x3))
|
||||||
|
return (x3, y3)
|
||||||
|
|
||||||
|
|
||||||
|
def _ec_mul(k: int, p: tuple[int, int] | None) -> tuple[int, int] | None:
|
||||||
|
"""Scalar multiplication k*P using double-and-add."""
|
||||||
|
if k == 0 or p is _INF:
|
||||||
|
return _INF
|
||||||
|
|
||||||
|
result = _INF
|
||||||
|
addend = p
|
||||||
|
while k:
|
||||||
|
if k & 1:
|
||||||
|
result = _ec_add(result, addend)
|
||||||
|
addend = _ec_double(addend)
|
||||||
|
k >>= 1
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# ECDSA-SHA256 verification
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _modinv(a: int, m: int) -> int:
|
||||||
|
"""Modular inverse of a modulo m (integers, not GF(2^m))."""
|
||||||
|
if a < 0:
|
||||||
|
a = a % m
|
||||||
|
g, x, _ = _extended_gcd(a, m)
|
||||||
|
if g != 1:
|
||||||
|
raise ValueError("modular inverse does not exist")
|
||||||
|
return x % m
|
||||||
|
|
||||||
|
|
||||||
|
def _extended_gcd(a: int, b: int) -> tuple[int, int, int]:
|
||||||
|
"""Extended Euclidean algorithm for integers."""
|
||||||
|
if a == 0:
|
||||||
|
return b, 0, 1
|
||||||
|
g, x, y = _extended_gcd(b % a, a)
|
||||||
|
return g, y - (b // a) * x, x
|
||||||
|
|
||||||
|
|
||||||
|
def ecdsa_verify_sha256(
|
||||||
|
message: bytes,
|
||||||
|
signature_rs: bytes,
|
||||||
|
public_key_xy: bytes,
|
||||||
|
) -> bool:
|
||||||
|
"""Verify ECDSA-SHA256 signature on sect233r1.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: The data that was signed.
|
||||||
|
signature_rs: 60 bytes (r || s, each 30 bytes big-endian).
|
||||||
|
public_key_xy: 60 bytes (x || y, each 30 bytes big-endian).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the signature is valid.
|
||||||
|
"""
|
||||||
|
if len(signature_rs) != 60:
|
||||||
|
return False
|
||||||
|
if len(public_key_xy) != 60:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Parse signature
|
||||||
|
r = int.from_bytes(signature_rs[:30], "big")
|
||||||
|
s = int.from_bytes(signature_rs[30:], "big")
|
||||||
|
|
||||||
|
# Parse public key
|
||||||
|
qx = int.from_bytes(public_key_xy[:30], "big")
|
||||||
|
qy = int.from_bytes(public_key_xy[30:], "big")
|
||||||
|
q_point = (qx, qy)
|
||||||
|
|
||||||
|
# Check r, s in [1, n-1]
|
||||||
|
if not (1 <= r < _N and 1 <= s < _N):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Compute hash
|
||||||
|
h = hashlib.sha256(message).digest()
|
||||||
|
e = int.from_bytes(h, "big")
|
||||||
|
# Truncate to bit length of n
|
||||||
|
if 256 > _N_BITLEN:
|
||||||
|
e >>= 256 - _N_BITLEN
|
||||||
|
|
||||||
|
# Compute w = s^(-1) mod n
|
||||||
|
w = _modinv(s, _N)
|
||||||
|
|
||||||
|
# Compute u1 = e*w mod n, u2 = r*w mod n
|
||||||
|
u1 = (e * w) % _N
|
||||||
|
u2 = (r * w) % _N
|
||||||
|
|
||||||
|
# Compute R = u1*G + u2*Q
|
||||||
|
g_point = (_Gx, _Gy)
|
||||||
|
r_point = _ec_add(_ec_mul(u1, g_point), _ec_mul(u2, q_point))
|
||||||
|
|
||||||
|
if r_point is _INF:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# v = R.x (as integer, already in GF(2^m) which is an integer)
|
||||||
|
v = r_point[0] % _N
|
||||||
|
|
||||||
|
return v == r
|
||||||
Reference in New Issue
Block a user