ZATCA Fatoora Java SDK: Private Key Works in CLI but Fails in Java

:memo: Context
We are using the ZATCA Fatoora Java SDK (3.4.1) to sign invoices via:

com.gazt.einvoicing.signing.service.SigningService

  1. The private key works fine in the Fatoora CLI.
  2. When the same key is used explicitly in Java, signing fails.

:key: Private Key Instructions inside SDK

  1. File: Data/Certificates/ec-secp256k1-priv-key.pem
  2. Algorithm: EC secp256k1
  3. Format: Raw key only (without PEM headers/footers)

-----BEGIN EC PRIVATE KEY----- -----END EC PRIVATE KEY-----

:high_voltage: Behavior Observed

:one: Fatoora CLI

fatoora -sign -invoice Standard_Invoice.xml -signedInvoice Standard_Invoice_signed.xml

  1. Works successfully.
  2. Logs:

2025-10-28 01:31:46,272 [INFO] InvoiceSigningService - invoice has been signed successfully
2025-10-28 01:31:46,273 [INFO] InvoiceSigningService - *** INVOICE HASH = VL2jbK+A9E8eGmBkAPpgtI04YjyjRe+KE+v4zPjd8R4=
:two: Java SDK

  • Using the key via InputStream, signing fails.

  • Error:

2025-10-27 17:46:05,079 [http-nio-9095-exec-7] ERROR c.s.e.s.ServiceHome - Exception: org.bouncycastle.openssl.PEMException: unable to convert key pair: null

:key: Key Examples

A. Generic key that works in Java

  • First 50 chars: MHQCAQEEIIAfwgRIdi8IAgGfXWP/rh6QhYX0Pv8vCwB/70TSQ

B. Generated key using Fatoora SDK (fails in Java)

  • First 50 chars: MIGNAgEAMBAGByqGSM49AgEGBSuBBAAKBHYwdAIBAQQgYrv2Gx

We are not changing anything in the generated key from ZATCA Fatoora CSID generated private key.

Is there anything we are missing or does the private key generated through ZATCA Fatoora commands need some treatment to be used in JAVA.

@idaoud @Aturkistani request your input

I’m assuming you’re doing something like this (Kotlin):

        val key: String = ... // Load from file, trim any newlines
        val parser = PEMParser(InputStreamReader(ByteArrayInputStream(key.toByteArray(StandardCharsets.UTF_8))))
        val converter = JcaPEMKeyConverter()
        val pair = converter.getKeyPair(parser.readObject() as PEMKeyPair?)
        return pair.getPrivate()

The above would throw the exception you shared. To get it to work, you need to add the BEGIN/END lines to the key before passing it to BouncyCastle:

        val key: String = ... // Load from file, trim any newlines
        val fullKey = "-----BEGIN EC PRIVATE KEY-----\n" + key + "\n-----END EC PRIVATE KEY-----";
        val parser = PEMParser(InputStreamReader(ByteArrayInputStream(fullKey.toByteArray(StandardCharsets.UTF_8))))
        val converter = JcaPEMKeyConverter()
        val pair = converter.getKeyPair(parser.readObject() as PEMKeyPair?)
        return pair.getPrivate()

Alternatively, you can just use ECDSAUtil.loadPrivateKey() from com.zatca.sdk.util and pass it the key string loaded from file. It will add the header/footer lines automatically.

You can test this in a small console application using a hardcoded sandbox private key, i.e.

val key = "MHQCAQEEIL14JV+5nr/sE8Sppaf2IySovrhVBtt8+yz+g4NRKyz8oAcGBSuBBAAKoUQDQgAEoWCKa0Sa9FIErTOv0uAkC1VIKXxU9nPpx2vlf4yhMejy8c02XJblDq7tPydo8mq0ahOMmNo8gwni7Xt1KT9UeA=="
// .. The rest of the code