From 05224ccec1655ff3b78744a6868613609c316023 Mon Sep 17 00:00:00 2001 From: Sebastian Menge Date: Thu, 10 Sep 2026 01:38:22 +0200 Subject: [PATCH] fix: HNVSK carries the signature's security profile version (PIN:2 for two-step) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The encryption header was hardcoded to `PIN:1` while the signature header correctly sends `PIN:2` for a two-step TAN method. Consorsbank (BLZ 76030080) rejects that combination with `9010 Ungültiger Signaturaufbau: Fehler im Segmentaufbau` + `9800 Der Dialog wurde abgebrochen`, so no two-step dialog could ever be opened there. Evidence that the bank checks both headers: hbci4java commit 1982a78 (2019-09-17, "always send profile version 1") broke Consorsbank with this exact error and was reverted the same day in 9813da9; hbci4java has derived both headers from one getProfileVersion() ever since. python-fints has the mirror-image bug (HNSHK stuck at 1) and fails identically (raphaelm/python-fints#99). Measured against the live bank with this change: the dialog init is accepted (`0030 Auftrag empfangen - Sicherheitsfreigabe erforderlich`), the TAN step completes (`Angemeldet`), and HKSAL/HKKAZ follow with `3076`. DKB, ING and Postbank were not re-tested; the specification (FinTS 3.0 Security, PIN/TAN) defines the profile version per dialog, not per segment, so sending the same value in both headers is the conforming form. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_0113JyWvWThppT9YzAHQG6RA --- src/message.ts | 6 +++++- src/tests/message.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/message.ts b/src/message.ts index 4b4dfe7..26e5c4c 100644 --- a/src/message.ts +++ b/src/message.ts @@ -185,9 +185,13 @@ export class CustomerMessage extends Message { const now = new Date(); + // The encryption header carries the same security profile as the + // signature: PIN version 2 for a two-step TAN method, 1 for one-step. + // Some banks (Consorsbank, BLZ 76030080) reject a two-step dialog whose + // HNVSK still says PIN:1 with "9010 Ungültiger Signaturaufbau". const hnvsk: HNVSKSegment = { header: { segId: HNVSK.Id, segNr: 998, version: HNVSK.Version }, - secProfile: { secMethod: 'PIN', secVersion: 1 }, + secProfile: { secMethod: 'PIN', secVersion: firstSignature.secProfile.secVersion }, secFunc: 998, secRole: 1, secId: { partyType: 1, partyId: firstSignature.secId.partyId }, diff --git a/src/tests/message.test.ts b/src/tests/message.test.ts index b642547..e5357fc 100644 --- a/src/tests/message.test.ts +++ b/src/tests/message.test.ts @@ -104,4 +104,27 @@ describe('CustomerMessage', () => { const _message = Message.decode(encodedMessage); }); + it('encrypts with the security profile version of the signature (two-step: PIN:2)', () => { + const customerMessage = new CustomerMessage('0', 1); + + customerMessage.addSegment(hkidn); + customerMessage.addSegment(hkvvb); + customerMessage.sign(280, '12030000', '12345678', '123', '0', 900, '12345'); + const encodedMessage = customerMessage.encode(); + + expect(encodedMessage).toContain("'HNVSK:998:3+PIN:2+998+"); + expect(encodedMessage).toContain('HNSHK:2:4+PIN:2+900+'); + }); + + it('encrypts with the security profile version of the signature (one-step: PIN:1)', () => { + const customerMessage = new CustomerMessage('0', 1); + + customerMessage.addSegment(hkidn); + customerMessage.addSegment(hkvvb); + customerMessage.sign(280, '12030000', '12345678', '123', '0'); + const encodedMessage = customerMessage.encode(); + + expect(encodedMessage).toContain("'HNVSK:998:3+PIN:1+998+"); + expect(encodedMessage).toContain('HNSHK:2:4+PIN:1+999+'); + }); });