"""AUTH1 command builder + Table 8-12 reader-signature input. Mirrors applet/src/test/.../ReaderSide.java buildTable812 + buildAuth1Data. """ from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature CLA = 0x80 INS_AUTH1 = 0x81 USAGE_READER_AUTH1 = bytes([0x41, 0x5D, 0x95, 0x69]) USAGE_UD_AUTH1 = bytes([0x4E, 0x88, 0x7B, 0x4C]) def build_table_812( *, reader_id_32: bytes, credential_ephem_pub_x: bytes, reader_ephem_pub_x: bytes, transaction_id: bytes, ) -> bytes: return _build_table_8_12_or_13( reader_id_32, credential_ephem_pub_x, reader_ephem_pub_x, transaction_id, USAGE_READER_AUTH1, ) def build_table_813( *, reader_id_32: bytes, credential_ephem_pub_x: bytes, reader_ephem_pub_x: bytes, transaction_id: bytes, ) -> bytes: """Same TLV shape as Table 8-12, only the usage tag differs.""" return _build_table_8_12_or_13( reader_id_32, credential_ephem_pub_x, reader_ephem_pub_x, transaction_id, USAGE_UD_AUTH1, ) def _build_table_8_12_or_13( rid: bytes, cred_x: bytes, reader_x: bytes, txn: bytes, usage: bytes, ) -> bytes: # Match auth0.py / build_auth1_data discipline: validate fixed-length # inputs up front. Java callers get an ArrayIndexOutOfBoundsException # naturally; Python slicing would silently produce a wrong-length blob. if len(rid) != 32: raise ValueError(f"reader_id_32 must be 32B (got {len(rid)})") if len(cred_x) != 32: raise ValueError(f"credential_ephem_pub_x must be 32B (got {len(cred_x)})") if len(reader_x) != 32: raise ValueError(f"reader_ephem_pub_x must be 32B (got {len(reader_x)})") if len(txn) != 16: raise ValueError(f"transaction_id must be 16B (got {len(txn)})") return ( bytes([0x4D, 0x20]) + rid + bytes([0x86, 0x20]) + cred_x + bytes([0x87, 0x20]) + reader_x + bytes([0x4C, 0x10]) + txn + bytes([0x93, 0x04]) + usage ) def sign_table_812_raw(table_812: bytes, reader_priv: ec.EllipticCurvePrivateKey) -> bytes: """Returns 64B raw r||s ECDSA-SHA-256 signature.""" der = reader_priv.sign(table_812, ec.ECDSA(hashes.SHA256())) r, s = decode_dss_signature(der) return r.to_bytes(32, "big") + s.to_bytes(32, "big") def build_auth1_data(raw_sig_64: bytes, command_parameters: int = 0x01) -> bytes: """AUTH1 command data field per spec Table 8-10. cmd_params bit 0 = 1 means 'return credential_PubK' (simpler than the SHA-1 key_slot path).""" if len(raw_sig_64) != 64: raise ValueError("reader signature must be 64B raw r||s") return ( bytes([0x41, 0x01, command_parameters]) + bytes([0x9E, 0x40]) + raw_sig_64 ) def build_auth1_apdu(data: bytes) -> bytes: return bytes([CLA, INS_AUTH1, 0x00, 0x00, len(data)]) + data