ECC P-256 problem (KSeF)

We are using TMSCryptographyPack in the C++ Builder 10.2 Tokyo environment to integrate with KSeF in Poland.

At the end of our development work, we encountered a certain problem.
We have the following code for generating a signature using the TECCEncSign object:

UnicodeString dir, filename, cipher;
dir = ExtractFilePath(Application->ExeName);

// This line makes error - Public key incorrect, must be 88
// ecc->FromCertificateFile("C:\Users\Andrzej\Desktop\Drugi link\certyfikat\certyfikat.crt");

Memo1->Lines->Clear();
Memo1->Lines->LoadFromFile(dir + "\test.txt", TEncoding::UTF8);
ecc->FromPrivateKeyFile("C:\Users\Andrzej\Desktop\Drugi link\certyfikat\dcertyfikat.key");
cipher = ecc->Sign(Memo1->Lines->Text.Trim());

Memo1->Lines->Clear();
Memo1->Lines->LoadFromFile(dir + "\test.txt");
Memo1->Lines->Text = Memo1->Lines->Text + "/" + cipher;

The test.txt file contains only the following text:
ksef-test.mf.gov.pl/client-app/certificate/Nip/6571508893/6571508893/014AE117D068BBB5/UtQp9Gpc51y-u3xApZjIjgkpZ01js-J8KflSPW8WzIE

After executing the code, we obtain the full link with the signature:
ksef-test.mf.gov.pl/client-app/certificate/Nip/6571508893/6571508893/014AE117D068BBB5/UtQp9Gpc51y-u3xApZjIjgkpZ01js-J8KflSPW8WzIE/BCh4oVStUv91pPbKkI6Jv6j6A2qAfVQmnoUo00EqAlcNgSc5yXCPUoiVS0YZQdRCRQjD5GgGk9i1rOWjuP5o2Kk=
where the string
BCh4oVStUv91pPbKkI6Jv6j6A2qAfVQmnoUo00EqAlcNgSc5yXCPUoiVS0YZQdRCRQjD5GgGk9i1rOWjuP5o2Kk=
is the generated signature appended to the link. This string has 88 characters (87 without = char at the end). By the way, shouldn’t the = character be automatically removed for Base64URL by the ecc object? We tested the link both with and without this character and it still doesn’t work, so for now this is a secondary issue.

The ecc component on the form is configured as follows:

  • ECCType: p256
  • outputFormat: base64url
  • NaCl: naclno
  • Unicode: yesUni

The problem is that the generated link is invalid.

For comparison, we generate the same signature using the online tool:

We paste our key, select P‑256, SHA‑256, and PEM for the key. We paste exactly the same text as in test.txt and we obtain a Base64 string such as:
MEUCIQCnyGX8GuEk8Bt/mrLXvodpQJnDoh4/5fvIHC9l26+vqgIgAkIaWxIG1TdI0EMxje8tFuYEkCmOijTs3gQGDCV84Nk=
After converting it to Base64URL, we get:
MEUCIQCnyGX8GuEk8Bt_mrLXvodpQJnDoh4_5fvIHC9l26-vqgIgAkIaWxIG1TdI0EMxje8tFuYEkCmOijTs3gQGDCV84Nk
and then the link is valid :grinning_face: :
ksef-test.mf.gov.pl/client-app/certificate/Nip/6571508893/6571508893/014AE117D068BBB5/UtQp9Gpc51y-u3xApZjIjgkpZ01js-J8KflSPW8WzIE/MEUCIQCnyGX8GuEk8Bt_mrLXvodpQJnDoh4_5fvIHC9l26-vqgIgAkIaWxIG1TdI0EMxje8tFuYEkCmOijTs3gQGDCV84Nk

We would like to point out that the signature generated by TMS Crypto has 88 characters, while the one generated by the online tool has 96 characters (95 characters after Base64URL conversion, because the trailing = disappears).
We kindly ask for guidance on what might be wrong here. How can we achieve compatibility between TECCEncSign and the online tool?

Best regards,
Andrzej Gąsowski

From another Polish user, it looks like the leading ‘04’ at the beginning of the signature shall be removed prior to submitting the file to KSeF.

This user kindly provided the following code that you may have to adapt for your case:

ecc.outputFormat := TConvertType.base64;
derBase64 := ecc.Sign('my_text_to_sign');
sigBytes := DecodeBase64(derBase64);

if (Length(sigBytes) = 65) and (sigBytes[0] = $04) then
  RawSig := Copy(sigBytes, 1, 64)
else if (Length(sigBytes) = 64) then
  RawSig := sigBytes;

Base64UrlTxt := EncodeBase64(RawSig, Length(RawSig));
Base64UrlTxt := Base64UrlEncode(Base64UrlTxt);

Best regards,

bernard

Hi,

Thank you for the quick reply. I’ll try to check it today.

Code updated:

UnicodeString dir, filename, cipher;

TBytes sigBytes;
TBytes RawSig;

sigBytes.Length = 0;
RawSig.Length = 0;

dir = ExtractFilePath(Application->ExeName);

// This line makes error - Public key incorrect, must be 88
// ecc->FromCertificateFile("C:\\Users\\Andrzej\\Desktop\\KSeF\\Drugi link\\certyfikat\\certyfikat.crt");

// loading source text to sign
Memo1->Lines->Clear();
Memo1->Lines->LoadFromFile(dir + "\\test.txt", TEncoding::UTF8);

// setting outputFormat and PrivateKey
ecc->outputFormat = base64;
ecc->FromPrivateKeyFile
	("C:\\Users\\Andrzej\\Desktop\\KSeF\\Drugi link\\certyfikat\\dcertyfikat.key"
	);

// sign text
cipher = ecc->Sign(Memo1->Lines->Text.Trim());

// convert text to bytes
sigBytes = TNetEncoding::Base64->DecodeStringToBytes(cipher);

// your suggestion
if ((sigBytes.Length == 65) && (sigBytes[0] == 0x04)) {
	RawSig = sigBytes;
	RawSig.Length = 64;
}
else if (sigBytes.Length == 64) {
	RawSig = sigBytes;
};

// bytes to UnicodeString
UnicodeString Base64UrlTxt = TNetEncoding::Base64->EncodeBytesToString
	(&RawSig[0], RawSig.Length);

// Base64 to Base64Url
Base64UrlTxt = StringReplace(Base64UrlTxt, L"+", L"-",
	TReplaceFlags() << rfReplaceAll);
Base64UrlTxt = StringReplace(Base64UrlTxt, L"/", L"_",
	TReplaceFlags() << rfReplaceAll);
while (Base64UrlTxt.Length() > 0 && Base64UrlTxt[Base64UrlTxt.Length()]
	== L'=') {
	Base64UrlTxt.SetLength(Base64UrlTxt.Length() - 1);
};

Memo1->Lines->Clear();
Memo1->Lines->LoadFromFile(dir + "\\test.txt");
// Memo1->Lines->Text = Memo1->Lines->Text + "/" + cipher;
Memo1->Lines->Text = Memo1->Lines->Text + "/" + Base64UrlTxt;

Unfortunately, it still doesn’t work correctly.

Example link: ksef-test.mf.gov.pl/client-app/certificate/Nip/6571508893/6571508893/014AE117D068BBB5/UtQp9Gpc51y-u3xApZjIjgkpZ01js-J8KflSPW8WzIE/BDnLlaf0s-1NkHyZADPubY6icFTPdsvKvqlOp456kcpscYOE7GN8ey-GXYNU1CcR0W79wWeDRheznp3qjS4IVQA

It seems that the Sign method isn’t working correctly. The online tool that generates a valid signature produces a longer string.

Hi, the issue is I don’t have access to KSef services and can only verify signed files with EU tools such as https://signatures-conformance-checker.etsi.org/login or DSS Demonstration WebApp

Current signatures pass thoses tests (except for the Spanish ones with SHA1).

Does your signature pass the ETSI checker? If yes, then, KSeF requires an adaptation that I cannot check myself.

I can put you in in touch with users who have successfully done it, if you wish.

Regards,

bernard

Hi,

Testing the part we are having trouble with does not require access to KSeF.
In a moment I will send you our private key for the test environment by email.

  1. Using this key, generate a signature for the following string:
    ksef-test.mf.gov.pl/client-app/certificate/Nip/6571508893/6571508893/014AE117D068BBB5/UtQp9Gpc51y-u3xApZjIjgkpZ01js-J8KflSPW8WzIE
  2. Append the generated result to the end of the above URL (after the slash):
    ksef-test.mf.gov.pl/client-app/certificate/Nip/6571508893/6571508893/014AE117D068BBB5/UtQp9Gpc51y-u3xApZjIjgkpZ01js-J8KflSPW8WzIE/{Your signature of the string from step 1 using our certificate – in Base64URL}
    for example (when we use online tool):
    ksef-test.mf.gov.pl/client-app/certificate/Nip/6571508893/6571508893/014AE117D068BBB5/UtQp9Gpc51y-u3xApZjIjgkpZ01js-J8KflSPW8WzIE/MEUCIQCnyGX8GuEk8Bt_mrLXvodpQJnDoh4_5fvIHC9l26-vqgIgAkIaWxIG1TdI0EMxje8tFuYEkCmOijTs3gQGDCV84Nk
  3. Paste the resulting link from step 2 into any browser. If everything is correct, a green confirmation message will appear.

That’s all! :grinning_face:

Below are the signature requirements from the KSeF documentation - they may be useful for you:

ECDSA (P‑256/SHA‑256)
The string to be signed is hashed using the SHA‑256 algorithm, and then a signature is generated using an ECDSA private key based on the NIST P‑256 (secp256r1) curve, which must be selected when generating the CSR.

The signature value is a pair of integers (r, s). It may be encoded in one of two formats:

  • IEEE P1363 Fixed Field Concatenation – the recommended method due to the shorter and fixed‑length output. The format is simpler and shorter than DER. The signature is the concatenation R S (32 bytes each, big‑endian).

  • ASN.1 DER SEQUENCE (RFC 3279) – the signature is encoded as ASN.1 DER. The signature size is variable. We recommend using this format only when IEEE P1363 is not possible due to technological limitations.

Regards,
Andrzej

A small correction.

I wrote:
{Your signature of the string from step 1 using our certificate – in Base64URL}


I meant:
{Your signature of the string from step 1 using our key – in Base64URL}

Hi Andrzej,

Here is a solution that addresses both formats accepted by KSeF.

procedure TMainForm.KsefBtnClick(Sender: TObject);
var
s, t, u, sig: string;
Conv: TConvert;
I: integer;

begin
s := 'ksef-test.mf.gov.pl/client-app/certificate/Nip/6571508893/6571508893/014AE117D068BBB5/UtQp9Gpc51y-u3xApZjIjgkpZ01js-J8KflSPW8WzIE';
Conv := TConvert.Create(Base64url);
try
EC.ECCType := p256;
EC.outputFormat := raw;
EC.FromPrivateKeyFile('....\TestData\KSeFcertyfikat.key'); // Your KEY
sig := EC.Sign(s);
sig := sig.Substring(1, sig.Length - 1); // remove leading #4

// Display IEEE P1363 base64url encoded result
MainMemo.Lines.Add('P1363 signature: ' + Conv.CharToFormat(sig)); // the fomatted string without the trailing '=' (if present) can be added to the KSeF URL and verifies correctly

// Create ASN.1 DER SEQUENCE (RFC 3279)
for I := 1 to 32 do begin
  t := t + sig[I];
  u := u + sig[I + 32];
end;
if (byte(t[1]) and $80) = $80 then
  t := #0 + t;
if (byte(u[1]) and $80) = $80 then
  u := #0 + u;

t := #2 + char(t.Length) + t;
u := #2 + char(u.Length) + u;
sig := #$0030 + char(Length(t + u)) + t + u;
MainMemo.Lines.Add('RFC 3279 signature: ' + Conv.CharToFormat(sig)); // the formatted string can be added to the KSeF URL

finally
Conv.Free;
end;
end;

Examples (IEEE P1363 format then RFC 3279 format):

Let me know if it works for you.

Regards,

bernard

Thank you!

I will test this solution by tomorrow at the latest and let you know whether it works.

If it does, it will definitely help others. Generating a signed QR code is important for KSeF in Poland and it’s a hot topic right now. If this works, then the entire TMS Crypto library will be fully compliant with KSeF 2.0.

Well, it will work :slight_smile:

Note that you need to remove the tailing ‘=’ signs from the generated base64url text (if any).

I will add a specific function to the ECC class for this signature type.

Regarding signing QR codes, I did that in 2015, or so, in C at the time. I could certainly add that capability to TMS CP in full Delphi. If you have the detailed technical requirements, I will have a look at them.

Final code will look like this.

s := 'ksef-test.mf.gov.pl/client-app/certificate/Nip/6571508893/6571508893/014AE117D068BBB5/UtQp9Gpc51y-u3xApZjIjgkpZ01js-J8KflSPW8WzIE';
try
EC.FromPrivateKeyFile('....\TestData\KSeFcertyfikat.key'); // Your KEY
sig := EC.SignWithKsefFormat(s, ieeeP1363);

MainMemo.Lines.Add(s + '/' + sig);
sig := EC.SignWithKsefFormat(s, rfc3279);

MainMemo.Lines.Add(s + '/' + sig);

finally

end;


s + '/' + sig can be submitted directly to KSeF

Example results (IEEE then RFC):

ksef-test.mf.gov.pl/client-app/certificate/Nip/6571508893/6571508893/014AE117D068BBB5/UtQp9Gpc51y-u3xApZjIjgkpZ01js-J8KflSPW8WzIE/AqWdiM1AHlQ9NuWlkx5v2bsnHkHA71hYPJtkz3RGvjF0C72dL3NiK63tJQEk2aEuNfg9rsKeK7pH4s5ixmYuig

ksef-test.mf.gov.pl/client-app/certificate/Nip/6571508893/6571508893/014AE117D068BBB5/UtQp9Gpc51y-u3xApZjIjgkpZ01js-J8KflSPW8WzIE/MEQCIA7eXm3qF8_6zM_raezv0yT-XTVJsYKbsmzYI1m_axozAiARVt1latnwAwm9OnLhq4BMBQjjFDrZ9cfSG93T4-eCIA

Thank you very much, Bernard!

Your code, after being translated to C++ Builder and slightly modified, works well.
It seems that the issue was indeed the leading #4.

My final C++ Builder code looks like this:

UnicodeString s, sig;

s = "ksef-test.mf.gov.pl/client-app/certificate/Nip/6571508893/6571508893/014AE117D068BBB5/UtQp9Gpc51y-u3xApZjIjgkpZ01js-J8KflSPW8WzIE";

ecc->ECCType = p256;

// This line produces error - Private length incorrect, must be 32
// ecc->outputFormat = raw;

ecc->outputFormat = hexa;
ecc->FromPrivateKeyFile
	("C:\\Users\\Andrzej\\Desktop\\KSeF\\Drugi link\\certyfikat\\dcertyfikat.key"
	);

Memo1->Lines->Clear();
Memo1->Lines->Add(s);
Memo1->Lines->Add("");

sig = ecc->Sign(s);
Memo1->Lines->Add(sig);
Memo1->Lines->Add("");

sig = sig.SubString(3, sig.Length() - 2); // remove leading #4
Memo1->Lines->Add(sig);
Memo1->Lines->Add("");

conv->AType = hexa;
sig = conv->HexaToBase64url(sig);

Memo1->Lines->Add(sig);
Memo1->Lines->Add("");

// Remove '=' characters
sig = StringReplace(sig, L"=", L"", TReplaceFlags() << rfReplaceAll);

// Display IEEE P1363 base64url encoded result
Memo1->Lines->Add(s + "/" + sig);

In the Memo component it produces, for example, the following:

ksef-test.mf.gov.pl/client-app/certificate/Nip/6571508893/6571508893/014AE117D068BBB5/UtQp9Gpc51y-u3xApZjIjgkpZ01js-J8KflSPW8WzIE

042B3B8F66928D9551D21B8C41259A32E6E019B09CBC7239BB423ACE44AED98A4D811E87F502D678A170905D267C31690B343FA1242F5A23065C742D66B45E4D6E

2B3B8F66928D9551D21B8C41259A32E6E019B09CBC7239BB423ACE44AED98A4D811E87F502D678A170905D267C31690B343FA1242F5A23065C742D66B45E4D6E

KzuPZpKNlVHSG4xBJZoy5uAZsJy8cjm7QjrORK7Zik2BHof1AtZ4oXCQXSZ8MWkLND-hJC9aIwZcdC1mtF5Nbg==

ksef-test.mf.gov.pl/client-app/certificate/Nip/6571508893/6571508893/014AE117D068BBB5/UtQp9Gpc51y-u3xApZjIjgkpZ01js-J8KflSPW8WzIE/KzuPZpKNlVHSG4xBJZoy5uAZsJy8cjm7QjrORK7Zik2BHof1AtZ4oXCQXSZ8MWkLND-hJC9aIwZcdC1mtF5Nbg

The test link for verifying the KSeF certificate generated this way works :grinning_face: !

IEEE P1363 is the recommended method, so I didn’t check the DER SEQUENCE (RFC 3279) anymore.

The only thing that worries me is the line of code:
ecc->outputFormat = raw;
In C++ Builder it throws an error at the moment the key is loaded:
Private length incorrect, must be 32

I think it’s worth taking a look at this when updating TMS Crypto.

In any case, if the outputFormat is set to hex, everything else works.

Excellent news!

I guess the issue with ‘raw’ vs ‘hexa’ is that the key is loaded as a hex string because some conversion was not passed in the C++ conversion.