mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-05-14 12:09:35 +00:00
137 lines
4.7 KiB
Python
137 lines
4.7 KiB
Python
from typing import Optional
|
|
|
|
from ecdsa.util import sigdecode_der
|
|
|
|
|
|
from .. import constants
|
|
from ..constants import DerivationOption, DerivationSource, SigningAlgorithm
|
|
from ..card_interface import CardInterface
|
|
from ..exceptions import InvalidStateError
|
|
from ..parsing import tlv
|
|
from ..parsing.keypath import KeyPath
|
|
from ..parsing.signature_result import SignatureResult
|
|
|
|
|
|
def sign(
|
|
card: CardInterface,
|
|
digest: bytes,
|
|
p1: DerivationOption = DerivationOption.CURRENT,
|
|
p2: SigningAlgorithm = SigningAlgorithm.ECDSA_SECP256K1,
|
|
derivation_path: Optional[str] = None
|
|
) -> SignatureResult:
|
|
"""
|
|
Sign a 32-byte digest using the specified key and signing algorithm.
|
|
|
|
This command sends the SIGN APDU to the Keycard and parses the response,
|
|
returning a structured `SignatureResult` object. The signature may be
|
|
returned as a DER-encoded structure, a raw 65-byte format including
|
|
the recovery ID, or an ECDSA template depending on card behavior.
|
|
|
|
Preconditions:
|
|
- Secure Channel must be opened (unless using PINLESS)
|
|
- PIN must be verified (unless using PINLESS)
|
|
- A valid keypair must be loaded on the card
|
|
- If P1=PINLESS, a PIN-less path must be configured
|
|
|
|
Args:
|
|
card (CardInterface): Active Keycard transport session.
|
|
digest (bytes): 32-byte hash to be signed.
|
|
p1 (DerivationOption): Key derivation option. One of:
|
|
- CURRENT: Sign with the currently loaded key
|
|
- DERIVE: Derive key for signing without changing current
|
|
- DERIVE_AND_MAKE_CURRENT: Derive and load for future use
|
|
- PINLESS: Use pre-defined PIN-less key without SC/PIN
|
|
p2 (SigningAlgorithm): Signing algorithm. Defaults to
|
|
ECDSA_SECP256K1. Other options include SCHNORR_BIP340.
|
|
derivation_path (Optional[str]): String-formatted BIP32 path
|
|
(e.g. "m/44'/60'/0'/0/0"). Required if `p1` uses derivation.
|
|
The source (master/parent/current) is inferred from the path
|
|
prefix.
|
|
|
|
Returns:
|
|
SignatureResult: Parsed signature result, including the signature
|
|
(DER or raw), algorithm, and optional recovery ID or public key.
|
|
|
|
Raises:
|
|
ValueError: If the digest is not 32 bytes or path is invalid.
|
|
InvalidStateError: If preconditions (PIN, SC) are not met.
|
|
APDUError: If the card returns an error (e.g., SW=0x6985).
|
|
"""
|
|
if p2 not in (
|
|
SigningAlgorithm.ECDSA_SECP256K1,
|
|
SigningAlgorithm.SCHNORR_BIP340
|
|
):
|
|
raise NotImplementedError(
|
|
f"Signature algorithm {p2} not supported"
|
|
)
|
|
|
|
if len(digest) != 32:
|
|
raise ValueError("Digest must be exactly 32 bytes")
|
|
|
|
if p1 != DerivationOption.PINLESS and not card.is_pin_verified:
|
|
raise InvalidStateError(
|
|
"PIN must be verified to sign with this derivation option")
|
|
|
|
data = digest
|
|
source = DerivationSource.MASTER
|
|
if p1 in (
|
|
DerivationOption.DERIVE,
|
|
DerivationOption.DERIVE_AND_MAKE_CURRENT
|
|
):
|
|
if not derivation_path:
|
|
raise ValueError("Derivation path cannot be empty")
|
|
key_path = KeyPath(derivation_path)
|
|
data += key_path.data
|
|
source = key_path.source
|
|
|
|
response = card.send_secure_apdu(
|
|
ins=constants.INS_SIGN,
|
|
p1=p1 | source,
|
|
p2=p2,
|
|
data=data
|
|
)
|
|
|
|
if response.startswith(b'\xA0'):
|
|
outer = tlv.parse_tlv(response)
|
|
inner = tlv.parse_tlv(outer[0xA0][0])
|
|
pub = inner.get(0x80, [None])[0]
|
|
|
|
if len(inner.get(0x80, [])) > 1:
|
|
return SignatureResult(
|
|
algo=p2,
|
|
digest=digest,
|
|
r=int.from_bytes(inner[0x80][1][:32], "big"),
|
|
s=int.from_bytes(inner[0x80][1][32:64], "big"),
|
|
recovery_id=-1,
|
|
public_key=pub
|
|
)
|
|
else:
|
|
der_bytes = (
|
|
b'\x30' +
|
|
len(inner[0x30][0]).to_bytes(1, 'big') +
|
|
inner[0x30][0]
|
|
)
|
|
signature = sigdecode_der(der_bytes, 0)
|
|
r, s = signature
|
|
return SignatureResult(
|
|
algo=p2,
|
|
digest=digest,
|
|
r=r,
|
|
s=s,
|
|
public_key=pub
|
|
)
|
|
elif response.startswith(b'\x80'):
|
|
outer = tlv.parse_tlv(response)
|
|
raw = outer[0x80][0]
|
|
if len(raw) != 65:
|
|
raise ValueError("Expected 65-byte raw signature (r||s||recId)")
|
|
return SignatureResult(
|
|
algo=p2,
|
|
digest=digest,
|
|
r=int.from_bytes(raw[:32], "big"),
|
|
s=int.from_bytes(raw[32:64], "big"),
|
|
recovery_id=int(raw[64])
|
|
)
|
|
|
|
raise ValueError("Unexpected SIGN response format")
|