Dzień dobry.
Mam oto taki kod, który ma za zadanie zalogować się do środowiska testowego KSeF. Niestety cały czas otrzymuje błąd 9105 "Nieprawidłowy podpis".
Certyfikat (.crt) z kluczem (.key) wygenerowałem wcześniej na tej stronie: https://web2te-ksef.mf.gov.pl/web/home
Proszę o pomoc i wskazanie gdzie leży błąd, bo nawet AI sobie nie radzi 
Z góry dzięki :)
import requests
import base64
from lxml import etree
from signxml import XMLSigner
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
class KSeFClient:
def init(self, nip, cert_path, key_path, key_password, environment='test'):
self.nip = nip
self.cert_path = cert_path
self.key_path = key_path
self.key_password = key_password
self.base_url = "https://api-test.ksef.mf.gov.pl/v2" if environment == 'test' else "https://api.ksef.mf.gov.pl/v2"
def get_challenge(self):
print("[1/4] Pobieranie challenge...")
url = f"{self.base_url}/auth/challenge"
payload = {"contextIdentifier": {"type": "on", "identifier": self.nip}}
response = requests.post(url, json=payload)
response.raise_for_status()
data = response.json()
print(f" -> Challenge: {data['challenge']}")
return data['challenge']
def load_private_key_safely(self):
print("[2/4] Wczytywanie klucza...")
try:
with open(self.key_path, "r", encoding='utf-8', errors='ignore') as f:
raw_content = f.read().strip()
if '\ufeff' in raw_content: raw_content = raw_content.replace('\ufeff', '')
header = "-----BEGIN ENCRYPTED PRIVATE KEY-----"
footer = "-----END ENCRYPTED PRIVATE KEY-----"
if header not in raw_content: raise ValueError("Brak nagłówka klucza")
body = raw_content.replace(header, "").replace(footer, "").strip()
body = "".join(body.split())
clean_pem = f"{header}\n{body}\n{footer}".encode('utf-8')
return serialization.load_pem_private_key(clean_pem, password=self.key_password.encode('utf-8'))
except Exception as e:
print(f" [BLAD KLUCZA] {e}")
raise
def create_signed_xml(self, challenge, private_key):
print("[3/4] Podpisywanie XML (ECDSA-SHA256)...")
# 1. Namespace KSeF 2.0
ns_ksef = "http://ksef.mf.gov.pl/auth/token/2.0"
ns_map = {None: ns_ksef, 'xsi': "http://www.w3.org/2001/XMLSchema-instance"}
root = etree.Element(f"{{{ns_ksef}}}AuthTokenRequest", nsmap=ns_map)
etree.SubElement(root, f"{{{ns_ksef}}}Challenge").text = challenge
ctx = etree.SubElement(root, f"{{{ns_ksef}}}ContextIdentifier")
etree.SubElement(ctx, f"{{{ns_ksef}}}Nip").text = self.nip
etree.SubElement(root, f"{{{ns_ksef}}}SubjectIdentifierType").text = "certificateSubject"
with open(self.cert_path, "rb") as f:
cert_data = f.read()
# 2. KONFIGURACJA SIGNERA - PEŁNE ADRESY URI (TO JEST KLUCZ DO SUKCESU)
# KSeF wymaga konkretnych stringów w <SignedInfo>, skróty nie działają.
signer = XMLSigner(
signature_algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
digest_algorithm="http://www.w3.org/2001/04/xmlenc#sha256",
c14n_algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"
)
signed_root = signer.sign(root, key=private_key, cert=cert_data)
# 3. Weryfikacja formatu RAW (64 bajty)
ns_dsig = {"ds": "http://www.w3.org/2000/09/xmldsig#"}
sig_value_node = signed_root.find(".//ds:SignatureValue", namespaces=ns_dsig)
if sig_value_node is not None:
current_sig = base64.b64decode(sig_value_node.text)
if len(current_sig) != 64:
# Jeśli signxml wypluł DER (np. 70-72 bajty), konwertujemy na RAW
try:
r, s = decode_dss_signature(current_sig)
raw_sig = r.to_bytes(32, 'big') + s.to_bytes(32, 'big')
sig_value_node.text = base64.b64encode(raw_sig).decode('utf-8')
except Exception:
pass # Jeśli się nie uda, wysyłamy co mamy
# Zwracamy czysty XML
return etree.tostring(signed_root, encoding='utf-8', xml_declaration=True)
def login(self):
try:
challenge = self.get_challenge()
private_key = self.load_private_key_safely()
signed_xml = self.create_signed_xml(challenge, private_key)
print("[4/4] Wysyłanie do KSeF...")
url = f"{self.base_url}/auth/xades-signature"
headers = {"Content-Type": "application/xml", "Accept": "application/json"}
response = requests.post(url, data=signed_xml, headers=headers)
if response.status_code == 202:
print("\n" + "="*50)
print(" [!!!] SUKCES - TOKEN OTRZYMANY [!!!]")
print("="*50)
return response.json()
else:
print(f"\n[BŁĄD KSeF] Kod: {response.status_code}")
# Debug: wypisz odpowiedź, czasem zawiera podpowiedź w 'exceptionDescription'
try:
print(response.json())
except:
print(response.text)
return None
except Exception as e:
print(f"\n[STOP] {e}")
return None
if name == "main":
MOJ_NIP = "XXXXXXXXXX"
CERT_FILE = "XXXXX.crt"
KEY_FILE = "XXXXX.key"
HASLO = "XXXXX"
klient = KSeFClient(MOJ_NIP, CERT_FILE, KEY_FILE, HASLO)
result = klient.login()
if result:
print(f"Twój Token Sesyjny: {result['authenticationToken']['token']}")