mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-04-13 12:22:33 -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,
|
||||
keys: dict[str, dict[str, bytes]],
|
||||
) -> 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
|
||||
|
||||
ECC certificate verification (sect233r1) is not reproduced — requires
|
||||
binary field curve arithmetic unavailable in standard Python libraries.
|
||||
AES decryption + SHA-256 hash is sufficient to prove file integrity.
|
||||
OTP body layout:
|
||||
0x000: u32 magic (0xDEADB00F)
|
||||
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).
|
||||
"""
|
||||
from sect233r1 import ecdsa_verify_sha256, _ec_mul, _Gx, _Gy, _N
|
||||
|
||||
data = bytearray(Path(filepath).read_bytes())
|
||||
|
||||
if len(data) != 0x100:
|
||||
@@ -336,7 +346,71 @@ def verify_otp(
|
||||
if computed_hash != stored_hash:
|
||||
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"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
257
scripts/sect233r1.py
Normal file
257
scripts/sect233r1.py
Normal file
@@ -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