jonesmarvin8 41f34f4ff4 fixes
2026-04-26 20:27:22 -04:00

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")