diff --git a/src/crypto/address.rs b/src/crypto/address.rs index 1355c10..da7a8b0 100644 --- a/src/crypto/address.rs +++ b/src/crypto/address.rs @@ -1,37 +1,36 @@ -//! Keeta address encoding: `keeta_{base32(key_type || pubkey || checksum[0..5])}` +//! Keeta address encoding: `keeta_{base32(key_type || raw_key || checksum)}`. +//! Mirrors node-rs `format_public_key_string(key_data, key_type)`. use super::kdf::sha3_impl::Sha3Hasher; -use super::params::Algorithm; use base32ct::{Base32Unpadded, Encoding}; +pub const ADDRESS_PREFIX: &[u8] = b"keeta_"; +pub const CHECKSUM_LEN: usize = 5; pub const MAX_ADDRESS_LEN: usize = 72; /// Returns the length of the generated address. -pub fn generate_address(pubkey: &[u8], algorithm: Algorithm, output: &mut [u8]) -> usize { - // Key type for address encoding matches Algorithm discriminant values - let key_type = algorithm as u8; - - // Build data to encode: key_type || pubkey || checksum[0..5] +pub fn generate_address(raw_key: &[u8], key_type: u8, output: &mut [u8]) -> usize { + // Build data to encode: key_type || raw_key || checksum[0..CHECKSUM_LEN] let mut data = [0u8; 64]; data[0] = key_type; - let pubkey_len = pubkey.len(); - data[1..1 + pubkey_len].copy_from_slice(pubkey); - // Calculate checksum: SHA3-256(key_type || pubkey) - let checksum = Sha3Hasher::hash(&data[..1 + pubkey_len]); + let key_len = raw_key.len(); + data[1..1 + key_len].copy_from_slice(raw_key); + + // Calculate checksum: SHA3-256(key_type || raw_key) + let checksum = Sha3Hasher::hash(&data[..1 + key_len]); - // Append first 5 bytes of checksum - let data_len = 1 + pubkey_len + 5; - data[1 + pubkey_len..data_len].copy_from_slice(&checksum[..5]); + // Append the truncated checksum + let data_len = 1 + key_len + CHECKSUM_LEN; + data[1 + key_len..data_len].copy_from_slice(&checksum[..CHECKSUM_LEN]); // Write prefix - let prefix = b"keeta_"; - output[..6].copy_from_slice(prefix); + let prefix_len = ADDRESS_PREFIX.len(); + output[..prefix_len].copy_from_slice(ADDRESS_PREFIX); // Base32 encode the data (RFC4648 lowercase, no padding) - let encoded = Base32Unpadded::encode(&data[..data_len], &mut output[6..]).unwrap(); - - 6 + encoded.len() + let encoded = Base32Unpadded::encode(&data[..data_len], &mut output[prefix_len..]).unwrap(); + prefix_len + encoded.len() } /// Compress secp pubkey to 33-byte format. @@ -76,7 +75,7 @@ mod tests { let pubkey = hex!("02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); let mut output = [0u8; MAX_ADDRESS_LEN]; - let len = generate_address(&pubkey, Algorithm::Secp256k1, &mut output); + let len = generate_address(&pubkey, 0x00, &mut output); let address = core::str::from_utf8(&output[..len]).unwrap(); // Format: keeta_ prefix + base32 @@ -86,9 +85,9 @@ mod tests { .skip(6) .all(|c| c.is_ascii_lowercase() || ('2'..='7').contains(&c))); - // Different algorithms produce different addresses + // Different key types produce different addresses let mut output_r1 = [0u8; MAX_ADDRESS_LEN]; - let len_r1 = generate_address(&pubkey, Algorithm::Secp256r1, &mut output_r1); + let len_r1 = generate_address(&pubkey, 0x06, &mut output_r1); assert_ne!(&output[..len], &output_r1[..len_r1]); } @@ -97,7 +96,7 @@ mod tests { let pubkey = [0x03u8; 32]; let mut output = [0u8; MAX_ADDRESS_LEN]; - let len = generate_address(&pubkey, Algorithm::Ed25519, &mut output); + let len = generate_address(&pubkey, 0x01, &mut output); assert!(output[..len].starts_with(b"keeta_")); // Ed25519 (32-byte key) produces shorter address than secp (33-byte) diff --git a/src/crypto/params.rs b/src/crypto/params.rs index 968cfec..540ff8b 100644 --- a/src/crypto/params.rs +++ b/src/crypto/params.rs @@ -22,23 +22,6 @@ impl TryFrom for Algorithm { } } -impl Algorithm { - /// Determine algorithm from public key prefix byte. - /// Keeta public keys are prefixed with the algorithm type byte. - /// Falls back to Secp256k1 for unknown prefixes. - pub fn from_pubkey_prefix(pubkey: &[u8]) -> Self { - if pubkey.is_empty() { - return Algorithm::Secp256k1; - } - match pubkey[0] { - 0x00 => Algorithm::Secp256k1, - 0x01 => Algorithm::Ed25519, - 0x06 => Algorithm::Secp256r1, - _ => Algorithm::Secp256k1, - } - } -} - // Curve orders (n) pub const SECP256K1_ORDER: [u8; 32] = [ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xBA, 0xAE, 0xDC, diff --git a/src/handlers/common.rs b/src/handlers/common.rs index c70f59d..a854727 100644 --- a/src/handlers/common.rs +++ b/src/handlers/common.rs @@ -66,9 +66,8 @@ pub fn derive_pubkey_and_address( } }; - // Generate address - let address_len = generate_address(&pubkey_bytes[..pubkey_len], algorithm, address_buf); - + // Generate address (key type = algorithm discriminant) + let address_len = generate_address(&pubkey_bytes[..pubkey_len], algorithm as u8, address_buf); Ok((pubkey_bytes, pubkey_len, address_len)) } diff --git a/src/handlers/sign_block.rs b/src/handlers/sign_block.rs index 4c7860a..7a26c84 100644 --- a/src/handlers/sign_block.rs +++ b/src/handlers/sign_block.rs @@ -1582,12 +1582,18 @@ fn format_external_permissions(external: u64, buf: &mut [u8]) -> usize { pos } -/// Format a public key to an address string using a static buffer. -/// Returns a string reference to the formatted address. -fn format_address_to_str<'a>(pubkey: &[u8], buf: &'a mut [u8]) -> &'a str { - unsafe { - let len = generate_address(pubkey, Algorithm::from_pubkey_prefix(pubkey), buf); - core::str::from_utf8_unchecked(&buf[..len]) +/// Format a raw key + key type to an address string using a static buffer. +fn address_to_str<'a>(raw_key: &[u8], key_type: u8, buf: &'a mut [u8]) -> &'a str { + let len = generate_address(raw_key, key_type, buf); + unsafe { core::str::from_utf8_unchecked(&buf[..len]) } +} + +/// Format an on-chain account value (`keyType || rawKey`) to an address +/// string, mirroring node-rs `to_public_key_string`. +fn format_address_to_str<'a>(account: &[u8], buf: &'a mut [u8]) -> &'a str { + match account.split_first() { + Some((key_type, raw_key)) => address_to_str(raw_key, *key_type, buf), + None => "", } } @@ -1618,7 +1624,12 @@ fn format_token_to_str<'a>(token_address: &[u8], buf: &'a mut [u8]) -> &'a str { } } - // Not in cache - fall back to full address + // Not in cache - fall back to full address. + // 33-byte tokens carry no key type; secp256k1 (0x00) is implied. + if token_address.len() == 33 { + return address_to_str(token_address, Algorithm::Secp256k1 as u8, buf); + } + format_address_to_str(token_address, buf) } diff --git a/tests/conftest.py b/tests/conftest.py index 6e36284..be3c54a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,12 @@ -"""Speculos fixtures and APDU constants.""" +"""Speculos fixtures, APDU constants, and reference-verifier runners.""" +import json import os +import shutil import socket +import subprocess import threading +from pathlib import Path import pytest import requests @@ -42,6 +46,76 @@ ) +FIXTURES_DIR = Path(__file__).parent / "fixtures" + +RUST_VERIFIER_BIN = ( + FIXTURES_DIR / "verify_signature_rs" / "target" / "release" / "verify_signature" +) + +TS_VERIFIER_DIR = FIXTURES_DIR / "verify_signature_ts" +TS_VERIFIER_SCRIPT = TS_VERIFIER_DIR / "verify_signature.ts" +TS_VERIFIER_TSX = TS_VERIFIER_DIR / "node_modules" / ".bin" / "tsx" +TS_VERIFIER_CLIENT = ( + TS_VERIFIER_DIR / "node_modules" / "@keetanetwork" / "keetanet-client" +) + + +def run_rust_verifier(args: list[str]) -> dict: + """Run the node-rs reference verifier and return its parsed JSON output.""" + if not RUST_VERIFIER_BIN.exists(): + pytest.fail( + f"Rust verifier not built at {RUST_VERIFIER_BIN}; " + f"run 'cargo build --release' in tests/fixtures/verify_signature_rs/" + ) + + result = subprocess.run( + [str(RUST_VERIFIER_BIN), *args], + capture_output=True, + text=True, + timeout=30, + check=False, + ) + if result.returncode != 0: + pytest.fail( + f"rust verifier failed for {' '.join(args[:2])}: " + f"{result.stderr.strip() or result.stdout.strip()}" + ) + + return json.loads(result.stdout) + + +def run_ts_verifier(args: list[str]) -> dict: + """Run the keetanet-client reference verifier and return its parsed JSON output.""" + if shutil.which("node") is None: + pytest.fail("node runtime not found on PATH; install Node.js 20") + + if not TS_VERIFIER_CLIENT.exists(): + pytest.fail( + f"@keetanetwork/keetanet-client not installed at {TS_VERIFIER_CLIENT}; " + f"run 'npm ci' in {TS_VERIFIER_DIR}" + ) + + if not TS_VERIFIER_TSX.exists(): + pytest.fail( + f"tsx not installed at {TS_VERIFIER_TSX}; run 'npm ci' in {TS_VERIFIER_DIR}" + ) + + result = subprocess.run( + [str(TS_VERIFIER_TSX), str(TS_VERIFIER_SCRIPT), *args], + capture_output=True, + text=True, + timeout=30, + check=False, + ) + if result.returncode != 0: + pytest.fail( + f"ts verifier failed for {' '.join(args[:2])}: " + f"{result.stderr.strip() or result.stdout.strip()}" + ) + + return json.loads(result.stdout) + + class SpeculosClient: """APDU client over raw TCP socket.""" diff --git a/tests/fixtures/verify_signature_rs/src/main.rs b/tests/fixtures/verify_signature_rs/src/main.rs index a5ca513..050b98d 100644 --- a/tests/fixtures/verify_signature_rs/src/main.rs +++ b/tests/fixtures/verify_signature_rs/src/main.rs @@ -30,9 +30,12 @@ impl Display for VerifyError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::WrongArgc => { - write!(f, "usage: verify_signature ") + write!( + f, + "usage: verify_signature \n verify_signature address " + ) } - Self::UnknownMode(v) => write!(f, "unknown mode '{v}': expected 'message' or 'block'"), + Self::UnknownMode(v) => write!(f, "unknown mode '{v}': expected 'message', 'block', or 'address'"), Self::UnknownAlgorithm(v) => { write!(f, "unknown algorithm '{v}': expected 'secp256k1', 'ed25519', or 'secp256r1'") } @@ -54,6 +57,7 @@ impl From for VerifyError { enum Mode { Message, Block, + Address, } impl FromStr for Mode { @@ -63,6 +67,7 @@ impl FromStr for Mode { match s { "message" => Ok(Self::Message), "block" => Ok(Self::Block), + "address" => Ok(Self::Address), _ => Err(VerifyError::UnknownMode(s.into())), } } @@ -125,38 +130,65 @@ fn verify(account: &GenericAccount, digest_input: &[u8], signature: &[u8]) -> bo } } -fn run(args: &[String]) -> Result { - if args.len() != 5 { - return Err(VerifyError::WrongArgc); - } - - let mode: Mode = args[0].parse()?; - let algorithm: SupportedAlgorithm = args[1].parse()?; - let pubkey_bytes = decode_hex("pubkey_hex", &args[2])?; - let input_bytes = decode_hex("input_hex", &args[3])?; - let sig_bytes = decode_hex("sig_hex", &args[4])?; +enum Output { + Valid(bool), + Address(String), +} +fn account_from_args(algorithm_arg: &str, pubkey_arg: &str) -> Result { + let algorithm: SupportedAlgorithm = algorithm_arg.parse()?; + let pubkey_bytes = decode_hex("pubkey_hex", pubkey_arg)?; let any_pubkey = parse_pubkey(&algorithm, &pubkey_bytes)?; - let account = GenericAccount::try_from(any_pubkey)?; - // Ledger signs SHA3(x). `verify` pre-hashes its input with SHA3. - // message: x = message. block: x = SHA3(block_bytes), so pass the block hash. - let digest_input: Vec = match mode { - Mode::Message => input_bytes, - Mode::Block => hash_default(&input_bytes).to_vec(), - }; + Ok(GenericAccount::try_from(any_pubkey)?) +} + +fn run(args: &[String]) -> Result { + let mode: Mode = args.first().ok_or(VerifyError::WrongArgc)?.parse()?; + + match mode { + Mode::Address => { + if args.len() != 3 { + return Err(VerifyError::WrongArgc); + } + + // `Display` for `GenericAccount` renders the canonical `keeta_...` address. + let account = account_from_args(&args[1], &args[2])?; + Ok(Output::Address(account.to_string())) + } + Mode::Message | Mode::Block => { + if args.len() != 5 { + return Err(VerifyError::WrongArgc); + } + + let account = account_from_args(&args[1], &args[2])?; + let input_bytes = decode_hex("input_hex", &args[3])?; + let sig_bytes = decode_hex("sig_hex", &args[4])?; + + // Ledger signs SHA3(x). `verify` pre-hashes its input with SHA3. + // message: x = message. block: x = SHA3(block_bytes), so pass the block hash. + let digest_input: Vec = match mode { + Mode::Block => hash_default(&input_bytes).to_vec(), + _ => input_bytes, + }; - Ok(verify(&account, &digest_input, &sig_bytes)) + Ok(Output::Valid(verify(&account, &digest_input, &sig_bytes))) + } + } } fn main() -> ExitCode { let args: Vec = std::env::args().skip(1).collect(); match run(&args) { - Ok(valid) => { + Ok(Output::Valid(valid)) => { println!("{{\"valid\":{valid}}}"); ExitCode::SUCCESS } + Ok(Output::Address(address)) => { + println!("{{\"address\":\"{address}\"}}"); + ExitCode::SUCCESS + } Err(e) => { eprintln!("verify_signature: {e}"); ExitCode::FAILURE diff --git a/tests/fixtures/verify_signature_ts/verify_signature.ts b/tests/fixtures/verify_signature_ts/verify_signature.ts index 1812b7d..a1736b2 100644 --- a/tests/fixtures/verify_signature_ts/verify_signature.ts +++ b/tests/fixtures/verify_signature_ts/verify_signature.ts @@ -5,7 +5,7 @@ import { createHash } from 'node:crypto'; import { lib } from '@keetanetwork/keetanet-client'; -type Mode = 'message' | 'block'; +type Mode = 'message' | 'block' | 'address'; type Algorithm = 'secp256k1' | 'ed25519' | 'secp256r1'; function die(msg: string): never { @@ -18,11 +18,11 @@ function sha3_256(input: Buffer): Buffer { } function parseMode(value: string): Mode { - if (value === 'message' || value === 'block') { + if (value === 'message' || value === 'block' || value === 'address') { return value; } - die(`unknown mode '${value}': expected 'message' or 'block'`); + die(`unknown mode '${value}': expected 'message', 'block', or 'address'`); } function parseAlgorithm(value: string): Algorithm { @@ -48,11 +48,9 @@ function toArrayBuffer(buf: Buffer): ArrayBuffer { function accountFromPubkey(algorithm: Algorithm, pubkeyHex: string): InstanceType { const Account = lib.Account; - if (algorithm === 'secp256k1') { return Account.fromECDSASECP256K1PublicKey(pubkeyHex); } - if (algorithm === 'ed25519') { return Account.fromED25519PublicKey(pubkeyHex); } @@ -62,12 +60,31 @@ function accountFromPubkey(algorithm: Algorithm, pubkeyHex: string): InstanceTyp async function main(): Promise { const argv = process.argv.slice(2); + if (argv.length < 1) { + die( + 'usage: verify_signature \n' + + ' verify_signature address ', + ); + } + + const mode = parseMode(argv[0]); + if (mode === 'address') { + if (argv.length !== 3) { + die('usage: verify_signature address '); + } + + const algorithm = parseAlgorithm(argv[1]); + const pubkeyBytes = parseHex('pubkey_hex', argv[2]); + const account = accountFromPubkey(algorithm, pubkeyBytes.toString('hex')); + + process.stdout.write(JSON.stringify({ address: account.publicKeyString.get() }) + '\n'); + return; + } if (argv.length !== 5) { - die('usage: verify_signature '); + die('usage: verify_signature '); } - const mode = parseMode(argv[0]); const algorithm = parseAlgorithm(argv[1]); const pubkeyBytes = parseHex('pubkey_hex', argv[2]); const inputBytes = parseHex('input_hex', argv[3]); diff --git a/tests/test_address_display.py b/tests/test_address_display.py new file mode 100644 index 0000000..9ca613e --- /dev/null +++ b/tests/test_address_display.py @@ -0,0 +1,147 @@ +"""Address display tests. + +Every `keeta_...` address the device emits (GET_PUBLIC_KEY response) or renders +(SIGN_BLOCK review screens) must match the canonical encoding produced by the +`node-rs` reference implementation (`verify_signature address` mode). +""" + +import re + +import pytest +import requests +from conftest import ( + ALG_ED25519, + ALG_SECP256K1, + ALG_SECP256R1, + CLA, + INS_GET_PUBLIC_KEY, + SET_REP_BLOCK, + SW_OK, + get_pubkey, + run_rust_verifier, + run_ts_verifier, + sign_block, +) + +# On-chain SetRep `to` value in SET_REP_BLOCK: keyType 0x00 || compressed secp256k1 key. +ORIGINAL_REP = bytes.fromhex( + "0003565af39d790ef8c12d48831ec5b3f78aa26b88cb2009020d895017d7022e527d" +) + +ALGORITHMS = [ + pytest.param("secp256k1", ALG_SECP256K1, id="secp256k1"), + pytest.param("ed25519", ALG_ED25519, id="ed25519"), + pytest.param("secp256r1", ALG_SECP256R1, id="secp256r1"), +] + +# Paginated review field title, e.g. "Account (1/2)" or "Repres...ive (2/2)". +_TITLE_RE = re.compile(r"(.+?) ?\((\d+)/(\d+)\)") +# Address chunk line: lowercase base32 with the keeta_ prefix underscore. +_CHUNK_RE = re.compile(r"[a-z0-9_]+") + + +def _canonical_address(algorithm: str, pubkey_hex: str) -> str: + """Canonical `keeta_...` encoding; both reference implementations must agree.""" + args = ["address", algorithm, pubkey_hex] + + rs_address = run_rust_verifier(args)["address"] + ts_address = run_ts_verifier(args)["address"] + + assert rs_address == ts_address, ( + f"reference drift for {algorithm}: " + f"node-rs={rs_address} keetanet-client={ts_address}" + ) + return rs_address + + +def _paginated_fields(texts: list[str]) -> dict[str, str]: + """Reassemble paginated review fields from screen text events.""" + fields: dict[str, str] = {} + current = None + + for text in texts: + title = _TITLE_RE.fullmatch(text) + if title: + current = title.group(1).strip() + fields.setdefault(current, "") + continue + + if current and _CHUNK_RE.fullmatch(text): + fields[current] += text + else: + current = None + + return fields + + +def _wrap_rep(rep: bytes) -> bytes: + """DER-wrap a SetRep `to` value: SEQUENCE > [1] > SEQUENCE > OCTET STRING.""" + octets = bytes([0x04, len(rep)]) + rep + inner_seq = bytes([0x30, len(octets)]) + octets + operations = bytes([0xA1, len(inner_seq)]) + inner_seq + return bytes([0x30, len(operations)]) + operations + + +def _block_with_rep(rep: bytes) -> bytes: + """SET_REP_BLOCK with its representative field replaced by `rep`.""" + old_region = _wrap_rep(ORIGINAL_REP) + new_region = _wrap_rep(rep) + + assert SET_REP_BLOCK.count(old_region) == 1 + patched = SET_REP_BLOCK.replace(old_region, new_region) + + # Fix the outer SEQUENCE length (long-form `30 81 ` header). + delta = len(new_region) - len(old_region) + return patched[:2] + bytes([patched[2] + delta]) + patched[3:] + + +def _field_by_prefix(fields: dict[str, str], prefix: str) -> str: + """Look up a field whose (possibly truncated) title starts with `prefix`.""" + for name, value in fields.items(): + if name.startswith(prefix): + return value + + pytest.fail(f"no review field titled '{prefix}*' in {sorted(fields)}") + + +@pytest.mark.parametrize("algorithm_name,algorithm_id", ALGORITHMS) +def test_get_public_key_address_is_canonical(client, algorithm_name, algorithm_id): + """GET_PUBLIC_KEY's address field must be the canonical node encoding.""" + response, sw = client.send_apdu( + CLA, INS_GET_PUBLIC_KEY, 0x00, algorithm_id, (0).to_bytes(4, "big") + ) + assert sw == SW_OK + + pubkey_len = response[0] + pubkey_hex = response[1 : 1 + pubkey_len].hex() + address_len = response[1 + pubkey_len] + address = response[2 + pubkey_len : 2 + pubkey_len + address_len].decode() + + assert address == _canonical_address(algorithm_name, pubkey_hex) + + +@pytest.mark.parametrize("algorithm_name,algorithm_id", ALGORITHMS) +def test_sign_block_review_addresses_are_canonical( + client, api_url, auto_approve, algorithm_name, algorithm_id +): + """SIGN_BLOCK review must render canonical addresses for every rep key type.""" + # Rep on-chain value: keyType || pubkey. ALG_* constants are the key types. + rep_pubkey = get_pubkey(client, index=1, algorithm=algorithm_id) + block = _block_with_rep(bytes([algorithm_id]) + rep_pubkey) + + requests.delete(f"{api_url}/events", timeout=2) + + sign_block(client, block) + + events = requests.get(f"{api_url}/events", timeout=5).json().get("events", []) + texts = [e["text"] for e in events if e.get("text")] + fields = _paginated_fields(texts) + + displayed_account = _field_by_prefix(fields, "Account") + displayed_rep = _field_by_prefix(fields, "Repres") + + # The "Account" review field shows the signer's own address. + signer_pubkey = get_pubkey(client) + + assert displayed_account == _canonical_address("secp256k1", signer_pubkey.hex()) + assert displayed_rep == _canonical_address(algorithm_name, rep_pubkey.hex()) diff --git a/tests/test_signature_round_trip.py b/tests/test_signature_round_trip.py index 801ec23..70eb269 100644 --- a/tests/test_signature_round_trip.py +++ b/tests/test_signature_round_trip.py @@ -5,11 +5,6 @@ `keetanetwork-account` (Rust). Rejection means a Ledger bug or reference drift. """ -import json -import shutil -import subprocess -from pathlib import Path - import pytest from conftest import ( ALG_ED25519, @@ -17,23 +12,12 @@ ALG_SECP256R1, SET_REP_BLOCK, get_pubkey, + run_rust_verifier, + run_ts_verifier, sign_block, sign_message, ) -FIXTURES_DIR = Path(__file__).parent / "fixtures" - -RUST_VERIFIER_BIN = ( - FIXTURES_DIR / "verify_signature_rs" / "target" / "release" / "verify_signature" -) - -TS_VERIFIER_DIR = FIXTURES_DIR / "verify_signature_ts" -TS_VERIFIER_SCRIPT = TS_VERIFIER_DIR / "verify_signature.ts" -TS_VERIFIER_TSX = TS_VERIFIER_DIR / "node_modules" / ".bin" / "tsx" -TS_VERIFIER_CLIENT = ( - TS_VERIFIER_DIR / "node_modules" / "@keetanetwork" / "keetanet-client" -) - # Non-32-byte to avoid SIGN_MESSAGE's block-hash-forgery guard. TEST_MESSAGE = b"round-trip test message" @@ -44,82 +28,13 @@ ] -def _run_rust_verifier( - mode: str, algorithm: str, pubkey_hex: str, input_hex: str, sig_hex: str -) -> bool: - if not RUST_VERIFIER_BIN.exists(): - pytest.fail( - f"Rust verifier not built at {RUST_VERIFIER_BIN}; " - f"run 'cargo build --release' in tests/fixtures/verify_signature_rs/" - ) - - result = subprocess.run( - [str(RUST_VERIFIER_BIN), mode, algorithm, pubkey_hex, input_hex, sig_hex], - capture_output=True, - text=True, - timeout=30, - check=False, - ) - if result.returncode != 0: - pytest.fail( - f"rust verifier failed for {algorithm}/{mode}: " - f"{result.stderr.strip() or result.stdout.strip()}" - ) - - return json.loads(result.stdout)["valid"] is True - - -def _run_ts_verifier( - mode: str, algorithm: str, pubkey_hex: str, input_hex: str, sig_hex: str -) -> bool: - node_bin = shutil.which("node") - if node_bin is None: - pytest.fail("node runtime not found on PATH; install Node.js 20") - - if not TS_VERIFIER_CLIENT.exists(): - pytest.fail( - f"@keetanetwork/keetanet-client not installed at {TS_VERIFIER_CLIENT}; " - f"run 'npm ci' in {TS_VERIFIER_DIR}" - ) - - if not TS_VERIFIER_TSX.exists(): - pytest.fail( - f"tsx not installed at {TS_VERIFIER_TSX}; run 'npm ci' in {TS_VERIFIER_DIR}" - ) - - result = subprocess.run( - [ - str(TS_VERIFIER_TSX), - str(TS_VERIFIER_SCRIPT), - mode, - algorithm, - pubkey_hex, - input_hex, - sig_hex, - ], - capture_output=True, - text=True, - timeout=30, - check=False, - ) - if result.returncode != 0: - pytest.fail( - f"ts verifier failed for {algorithm}/{mode}: " - f"{result.stderr.strip() or result.stdout.strip()}" - ) - - return json.loads(result.stdout)["valid"] is True - - def _assert_both_accept( mode: str, algorithm: str, pubkey: bytes, input_bytes: bytes, raw_sig: bytes ) -> None: - pubkey_hex = pubkey.hex() - input_hex = input_bytes.hex() - sig_hex = raw_sig.hex() + args = [mode, algorithm, pubkey.hex(), input_bytes.hex(), raw_sig.hex()] - rs_valid = _run_rust_verifier(mode, algorithm, pubkey_hex, input_hex, sig_hex) - ts_valid = _run_ts_verifier(mode, algorithm, pubkey_hex, input_hex, sig_hex) + rs_valid = run_rust_verifier(args)["valid"] is True + ts_valid = run_ts_verifier(args)["valid"] is True assert rs_valid, ( f"node-rs Account.verify rejected the Ledger's {algorithm} {mode} signature"