ebicsclient 0.2.0__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/.github/workflows/ci.yml +2 -2
  2. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/.github/workflows/release.yml +4 -4
  3. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/CLAUDE.md +6 -3
  4. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/PKG-INFO +45 -9
  5. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/README.md +43 -7
  6. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/examples/zkb_handshake.py +34 -1
  7. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/pyproject.toml +2 -2
  8. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/__init__.py +6 -0
  9. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/client.py +88 -12
  10. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/errors.py +9 -0
  11. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/models.py +44 -0
  12. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/protocol/h005.py +188 -3
  13. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/crypto_helpers.py +21 -4
  14. ebicsclient-1.3.0/tests/data/htd_zkb_sample.xml +707 -0
  15. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_client.py +121 -32
  16. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_h005.py +109 -1
  17. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_schema_validation.py +10 -0
  18. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/uv.lock +1 -1
  19. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/.gitignore +0 -0
  20. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/CONTRIBUTING.md +0 -0
  21. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/LICENSE.md +0 -0
  22. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/01-protocol-and-formats.md +0 -0
  23. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/02-licensing-strategy.md +0 -0
  24. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/03-library-landscape.md +0 -0
  25. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/04-implementation-plan.md +0 -0
  26. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/05-zkb-onboarding.md +0 -0
  27. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/06-engineering-conventions.md +0 -0
  28. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/07-handshake-testing.md +0 -0
  29. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/08-parity-and-xsd-findings.md +0 -0
  30. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/09-zkb-test-platform-settings.md +0 -0
  31. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/10-btf-order-types.md +0 -0
  32. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/11-certificate-profiles.md +0 -0
  33. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/certificates.py +0 -0
  34. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/crypto.py +0 -0
  35. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/formats/__init__.py +0 -0
  36. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/formats/camt.py +0 -0
  37. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/formats/camt052.py +0 -0
  38. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/formats/camt053.py +0 -0
  39. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/formats/camt054.py +0 -0
  40. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/formats/container.py +0 -0
  41. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/formats/pain002.py +0 -0
  42. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/keys.py +0 -0
  43. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/letter.py +0 -0
  44. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/protocol/__init__.py +0 -0
  45. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/py.typed +0 -0
  46. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/transport.py +0 -0
  47. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/data/camt052_zkb_sample.xml +0 -0
  48. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/data/camt053_zkb_sample.xml +0 -0
  49. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/data/camt054_collective_zkb_sample.xml +0 -0
  50. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/data/camt054_credit_zkb_sample.xml +0 -0
  51. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/data/camt054_debit_zkb_sample.xml +0 -0
  52. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/data/pain002_accp.xml +0 -0
  53. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/data/pain002_actc.xml +0 -0
  54. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/data/pain002_part.xml +0 -0
  55. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/data/pain002_rjct.xml +0 -0
  56. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/fixtures/parity/hia.xml +0 -0
  57. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/fixtures/parity/hpb.xml +0 -0
  58. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/fixtures/parity/ini.xml +0 -0
  59. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/fixtures/parity/meta.json +0 -0
  60. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_c14n_vectors.py +0 -0
  61. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_camt052_054.py +0 -0
  62. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_camt053.py +0 -0
  63. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_certificates.py +0 -0
  64. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_crypto.py +0 -0
  65. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_errors.py +0 -0
  66. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_keys.py +0 -0
  67. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_letter.py +0 -0
  68. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_pain002.py +0 -0
  69. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_php_parity.py +0 -0
  70. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_transport.py +0 -0
  71. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tools/fetch-schemas.py +0 -0
  72. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tools/php-parity/README.md +0 -0
  73. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tools/php-parity/composer.json +0 -0
  74. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tools/php-parity/composer.lock +0 -0
  75. {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tools/php-parity/generate.php +0 -0
@@ -13,10 +13,10 @@ jobs:
13
13
  matrix:
14
14
  python-version: ["3.11", "3.12", "3.13"]
15
15
  steps:
16
- - uses: actions/checkout@v4
16
+ - uses: actions/checkout@v6
17
17
 
18
18
  - name: Install uv
19
- uses: astral-sh/setup-uv@v5
19
+ uses: astral-sh/setup-uv@v7
20
20
  with:
21
21
  python-version: ${{ matrix.python-version }}
22
22
 
@@ -8,10 +8,10 @@ jobs:
8
8
  build:
9
9
  runs-on: ubuntu-latest
10
10
  steps:
11
- - uses: actions/checkout@v4
11
+ - uses: actions/checkout@v6
12
12
 
13
13
  - name: Install uv
14
- uses: astral-sh/setup-uv@v5
14
+ uses: astral-sh/setup-uv@v7
15
15
  with:
16
16
  python-version: "3.12"
17
17
 
@@ -42,7 +42,7 @@ jobs:
42
42
  - name: Build sdist and wheel
43
43
  run: uv build
44
44
 
45
- - uses: actions/upload-artifact@v4
45
+ - uses: actions/upload-artifact@v5
46
46
  with:
47
47
  name: dist
48
48
  path: dist/
@@ -58,7 +58,7 @@ jobs:
58
58
  # anywhere; PyPI trusts this repository + workflow + environment combination.
59
59
  id-token: write
60
60
  steps:
61
- - uses: actions/download-artifact@v4
61
+ - uses: actions/download-artifact@v6
62
62
  with:
63
63
  name: dist
64
64
  path: dist/
@@ -78,10 +78,13 @@ MVP = read **and** write, validated live against ZKB: key init (INI/HIA/HPB) →
78
78
  `EOP/camt.053.001.08` and parse balances → upload `MCT/pain.001.001.09` with the A006 electronic
79
79
  signature. All of this is built and bank-accepted.
80
80
 
81
+ The pain.002/camt.052/camt.054 parsers are built (M5, validated on genuine ZKB messages).
81
82
  Still deliberately **out of scope** — do not build until there is a concrete need and a way to
82
- validate it: distributed signatures (EDS), a pain.002 status-report parser, camt.052/camt.054
83
- reporting, non-H005 EBICS versions, and multi-bank quirk handling. The `protocol/` and `formats/`
84
- seams exist for these. Scope creep is what turns a stable standard into a maintenance burden.
83
+ validate it: distributed signatures (EDS the ZKB test subscriber has ``NumSigRequired=0``, so
84
+ EDS cannot be validated there) and multi-bank quirk handling. **H004 and earlier EBICS versions
85
+ will never be supported** (owner decision; see docs/04) the `protocol/` seam exists for a
86
+ future EBICS version, not for the past. Scope creep is what turns a stable standard into a
87
+ maintenance burden.
85
88
 
86
89
  ## Workflow
87
90
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ebicsclient
3
- Version: 0.2.0
3
+ Version: 1.3.0
4
4
  Summary: Pure-Python client for the EBICS 3.0 (H005) banking protocol
5
5
  Project-URL: Homepage, https://github.com/schulluk/ebicsclient
6
6
  Project-URL: Repository, https://github.com/schulluk/ebicsclient
@@ -95,7 +95,7 @@ License: Copyright © 2026 Lukas Schulze
95
95
  **Use** means anything you do with the software requiring one of your licenses.
96
96
  License-File: LICENSE.md
97
97
  Keywords: banking,camt,ebics,fintech,h005,iso20022
98
- Classifier: Development Status :: 4 - Beta
98
+ Classifier: Development Status :: 5 - Production/Stable
99
99
  Classifier: Intended Audience :: Developers
100
100
  Classifier: Intended Audience :: Financial and Insurance Industry
101
101
  Classifier: License :: Other/Proprietary License
@@ -118,9 +118,25 @@ Description-Content-Type: text/markdown
118
118
 
119
119
  # ebicsclient — a pure-Python EBICS 3.0 (H005) client
120
120
 
121
- A from-scratch, pure-Python client for the **EBICS** banking protocol. It **downloads account
122
- statements (camt.053)** and **initiates payments (pain.001)** over EBICS 3.0 / H005 — starting with
123
- Zürcher Kantonalbank (ZKB), against which the whole flow is validated live.
121
+ A from-scratch, pure-Python client for the **EBICS** banking protocol (EBICS 3.0 / H005),
122
+ validated live against Zürcher Kantonalbank (ZKB).
123
+
124
+ **What it provides today:**
125
+
126
+ - **Key ceremony** — INI/HIA/HPB with X.509 key transmission (self-signed *mit Schlüsseln* or
127
+ CA-issued *mit Zertifikaten*), the printable initialisation letter (HTML/PDF), and bank-key
128
+ **pinning** across sessions
129
+ - **Read** — statement/report downloads with parsers into typed models: **camt.053** end-of-day
130
+ statements, **camt.052** intraday reports, **camt.054** booking advices (incl. QRR/SCOR/LSV),
131
+ and **pain.002** payment status reports
132
+ - **Write** — **pain.001** payment submission (BTU) with the A006 electronic signature
133
+ - **Security throughout** — the bank's `AuthSignature` is verified on **every** response, unknown
134
+ return codes are never masked, and everything fails closed
135
+ - **Self-inspection** — `available_order_types()` (HAA) and `subscriber_info()` (HTD)
136
+
137
+ **What is missing:** distributed signatures (EDS/VEU — multi-person payment approval; see
138
+ milestone 7 below), and validation against banks beyond ZKB. Legacy EBICS versions (H004 and
139
+ earlier) are deliberately unsupported.
124
140
 
125
141
  - **Stack:** Python 3.11+, just two runtime deps — `cryptography` (RSA/AES) and `lxml` (XML /
126
142
  inclusive Canonical XML 1.0); everything else stdlib. No PHP/Java sidecar. (Rationale:
@@ -238,11 +254,31 @@ a real bank statement.
238
254
  - [x] camt.052 intraday reports
239
255
  - [x] camt.054 booking advices (incl. the QRR / SCOR / LSV variants via `service_option`)
240
256
 
241
- **Milestone 6 — Protocol conveniences & breadth**
257
+ **Milestone 6 — Protocol hardening & conveniences**
258
+
259
+ - [x] Verify the bank's `AuthSignature` on every response — validated live on ZKB
260
+ - [x] Subscriber self-inspection — `available_order_types()` (HAA) and `subscriber_info()` (HTD)
261
+
262
+ **Milestone 7 — Distributed signatures (EDS/VEU)** (next; parked until validatable)
263
+
264
+ EDS (*Elektronische Verteilte Unterschrift*) is EBICS's workflow for orders that need
265
+ **multiple people to sign before the bank executes them** — dual control on payments. An order
266
+ is uploaded with `requestEDS` and parks in the bank's VEU queue; further signatories list the
267
+ queue, inspect the order, and deliver their A006 signatures until the configured quorum (e.g.
268
+ first + second signature) is reached. The building blocks (A006 signing, the `SignatureFlag`,
269
+ the admin-download transaction pattern) are in place; what is missing is a **validatable
270
+ multi-user setup** — the ZKB test subscriber is single-user with `NumSigRequired=0`, so the
271
+ workflow cannot be exercised there. This milestone starts when a downstream setup with a real
272
+ multi-signature profile exists.
273
+
274
+ - [ ] Upload into the VEU queue (`SignatureFlag` with `requestEDS`)
275
+ - [ ] List pending orders (HVU/HVZ) and fetch order details (HVD) and transactions (HVT)
276
+ - [ ] Deliver an additional signature (HVE) and cancel a pending order (HVS)
277
+ - [ ] Validate the full quorum workflow against a multi-signature bank profile
242
278
 
243
- - [ ] Subscriber self-inspectionavailable order types (HAA) and subscriber info (HTD)
244
- - [ ] Distributed signatures (EDS)
245
- - [ ] Further EBICS versions (e.g. H004) via the `protocol/` seam
279
+ This client is **EBICS 3.0 (H005) only** legacy versions (H004 and earlier) will not be
280
+ supported (see [docs/04](docs/04-implementation-plan.md)); the `protocol/` seam exists for a
281
+ future EBICS version, not for the past.
246
282
 
247
283
  ## License
248
284
 
@@ -1,8 +1,24 @@
1
1
  # ebicsclient — a pure-Python EBICS 3.0 (H005) client
2
2
 
3
- A from-scratch, pure-Python client for the **EBICS** banking protocol. It **downloads account
4
- statements (camt.053)** and **initiates payments (pain.001)** over EBICS 3.0 / H005 — starting with
5
- Zürcher Kantonalbank (ZKB), against which the whole flow is validated live.
3
+ A from-scratch, pure-Python client for the **EBICS** banking protocol (EBICS 3.0 / H005),
4
+ validated live against Zürcher Kantonalbank (ZKB).
5
+
6
+ **What it provides today:**
7
+
8
+ - **Key ceremony** — INI/HIA/HPB with X.509 key transmission (self-signed *mit Schlüsseln* or
9
+ CA-issued *mit Zertifikaten*), the printable initialisation letter (HTML/PDF), and bank-key
10
+ **pinning** across sessions
11
+ - **Read** — statement/report downloads with parsers into typed models: **camt.053** end-of-day
12
+ statements, **camt.052** intraday reports, **camt.054** booking advices (incl. QRR/SCOR/LSV),
13
+ and **pain.002** payment status reports
14
+ - **Write** — **pain.001** payment submission (BTU) with the A006 electronic signature
15
+ - **Security throughout** — the bank's `AuthSignature` is verified on **every** response, unknown
16
+ return codes are never masked, and everything fails closed
17
+ - **Self-inspection** — `available_order_types()` (HAA) and `subscriber_info()` (HTD)
18
+
19
+ **What is missing:** distributed signatures (EDS/VEU — multi-person payment approval; see
20
+ milestone 7 below), and validation against banks beyond ZKB. Legacy EBICS versions (H004 and
21
+ earlier) are deliberately unsupported.
6
22
 
7
23
  - **Stack:** Python 3.11+, just two runtime deps — `cryptography` (RSA/AES) and `lxml` (XML /
8
24
  inclusive Canonical XML 1.0); everything else stdlib. No PHP/Java sidecar. (Rationale:
@@ -120,11 +136,31 @@ a real bank statement.
120
136
  - [x] camt.052 intraday reports
121
137
  - [x] camt.054 booking advices (incl. the QRR / SCOR / LSV variants via `service_option`)
122
138
 
123
- **Milestone 6 — Protocol conveniences & breadth**
139
+ **Milestone 6 — Protocol hardening & conveniences**
124
140
 
125
- - [ ] Subscriber self-inspection available order types (HAA) and subscriber info (HTD)
126
- - [ ] Distributed signatures (EDS)
127
- - [ ] Further EBICS versions (e.g. H004) via the `protocol/` seam
141
+ - [x] Verify the bank's `AuthSignature` on every response validated live on ZKB
142
+ - [x] Subscriber self-inspection — `available_order_types()` (HAA) and `subscriber_info()` (HTD)
143
+
144
+ **Milestone 7 — Distributed signatures (EDS/VEU)** (next; parked until validatable)
145
+
146
+ EDS (*Elektronische Verteilte Unterschrift*) is EBICS's workflow for orders that need
147
+ **multiple people to sign before the bank executes them** — dual control on payments. An order
148
+ is uploaded with `requestEDS` and parks in the bank's VEU queue; further signatories list the
149
+ queue, inspect the order, and deliver their A006 signatures until the configured quorum (e.g.
150
+ first + second signature) is reached. The building blocks (A006 signing, the `SignatureFlag`,
151
+ the admin-download transaction pattern) are in place; what is missing is a **validatable
152
+ multi-user setup** — the ZKB test subscriber is single-user with `NumSigRequired=0`, so the
153
+ workflow cannot be exercised there. This milestone starts when a downstream setup with a real
154
+ multi-signature profile exists.
155
+
156
+ - [ ] Upload into the VEU queue (`SignatureFlag` with `requestEDS`)
157
+ - [ ] List pending orders (HVU/HVZ) and fetch order details (HVD) and transactions (HVT)
158
+ - [ ] Deliver an additional signature (HVE) and cancel a pending order (HVS)
159
+ - [ ] Validate the full quorum workflow against a multi-signature bank profile
160
+
161
+ This client is **EBICS 3.0 (H005) only** — legacy versions (H004 and earlier) will not be
162
+ supported (see [docs/04](docs/04-implementation-plan.md)); the `protocol/` seam exists for a
163
+ future EBICS version, not for the past.
128
164
 
129
165
  ## License
130
166
 
@@ -15,6 +15,8 @@ Steps, matching the EBICS initialisation ceremony (see docs/05, docs/07):
15
15
  download fetch the EOP/camt.053 statements and print their closing balances
16
16
  upload upload a pain.001 payment file (path in EBICS_UPLOAD_PATH)
17
17
  pain002 fetch the pain.002 payment status report(s) and print them
18
+ haa list the order types with data available at the bank
19
+ htd show the subscriber data registered at the bank
18
20
 
19
21
  Run each step in order, e.g.::
20
22
 
@@ -83,7 +85,8 @@ def main() -> int:
83
85
  parser.add_argument(
84
86
  "step",
85
87
  choices=[
86
- "generate", "ini", "hia", "letter", "hpb", "hashes", "download", "upload", "pain002"
88
+ "generate", "ini", "hia", "letter", "hpb", "hashes", "download", "upload",
89
+ "pain002", "haa", "htd",
87
90
  ],
88
91
  )
89
92
  step = parser.parse_args().step
@@ -219,6 +222,34 @@ def _pain002() -> int:
219
222
  return 0
220
223
 
221
224
 
225
+ def _haa() -> int:
226
+ client = _build_client()
227
+ _fetch_bank_keys(client)
228
+ order_types = client.available_order_types()
229
+ if not order_types:
230
+ print("HAA: no order types currently have data available.")
231
+ return 0
232
+ print(f"HAA: {len(order_types)} order type(s) with data available:")
233
+ for btf in order_types:
234
+ print(f" {btf.service_name}/{btf.scope or '-'}/{btf.message_name}"
235
+ f" v{btf.message_version or '-'} container={btf.container or '-'}")
236
+ return 0
237
+
238
+
239
+ def _htd() -> int:
240
+ client = _build_client()
241
+ _fetch_bank_keys(client)
242
+ info = client.subscriber_info()
243
+ print(f"HTD: user {info.user_id} (status {info.user_status}), name: {info.name}")
244
+ print(f" permissions: {len(info.permissions)}, partner order types: {len(info.order_types)}")
245
+ for entry in info.order_types:
246
+ service = entry.service
247
+ btf = (f"{service.service_name}/{service.scope or '-'}/{service.message_name}"
248
+ f" v{service.message_version or '-'}") if service else "-"
249
+ print(f" {entry.admin_order_type:<4} {btf:<40} {entry.description or ''}")
250
+ return 0
251
+
252
+
222
253
  def _build_client() -> Client:
223
254
  return Client(_bank(), _user(), _load_keyring())
224
255
 
@@ -284,6 +315,8 @@ _STEPS = {
284
315
  "download": _download,
285
316
  "upload": _upload,
286
317
  "pain002": _pain002,
318
+ "haa": _haa,
319
+ "htd": _htd,
287
320
  }
288
321
 
289
322
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ebicsclient"
7
- version = "0.2.0"
7
+ version = "1.3.0"
8
8
  description = "Pure-Python client for the EBICS 3.0 (H005) banking protocol"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -12,7 +12,7 @@ license = { file = "LICENSE.md" }
12
12
  authors = [{ name = "Lukas Schulze" }]
13
13
  keywords = ["ebics", "banking", "iso20022", "camt", "h005", "fintech"]
14
14
  classifiers = [
15
- "Development Status :: 4 - Beta",
15
+ "Development Status :: 5 - Production/Stable",
16
16
  "Intended Audience :: Developers",
17
17
  "Intended Audience :: Financial and Insurance Industry",
18
18
  "License :: Other/Proprietary License",
@@ -30,6 +30,7 @@ from ebicsclient.errors import (
30
30
  MessageFormatError,
31
31
  MissingDependencyError,
32
32
  ProtocolError,
33
+ ResponseAuthenticationError,
33
34
  Retryability,
34
35
  ReturnCodeError,
35
36
  TransportError,
@@ -63,11 +64,13 @@ from ebicsclient.models import (
63
64
  Keyring,
64
65
  Letter,
65
66
  Notification,
67
+ OrderTypeInfo,
66
68
  OutputFormat,
67
69
  PaymentStatus,
68
70
  PaymentStatusReport,
69
71
  Statement,
70
72
  StatusReason,
73
+ SubscriberInfo,
71
74
  TransactionStatus,
72
75
  UploadPayload,
73
76
  User,
@@ -116,15 +119,18 @@ __all__ = [
116
119
  "MessageFormatError",
117
120
  "MissingDependencyError",
118
121
  "Notification",
122
+ "OrderTypeInfo",
119
123
  "OutputFormat",
120
124
  "PaymentStatus",
121
125
  "PaymentStatusReport",
122
126
  "ProtocolError",
127
+ "ResponseAuthenticationError",
123
128
  "Retryability",
124
129
  "ReturnCodeError",
125
130
  "SelfSignedCertificateProvider",
126
131
  "Statement",
127
132
  "StatusReason",
133
+ "SubscriberInfo",
128
134
  "TransactionStatus",
129
135
  "TransportError",
130
136
  "TrustAnchorVerifier",
@@ -32,6 +32,7 @@ from ebicsclient.models import (
32
32
  OutputFormat,
33
33
  PaymentStatusReport,
34
34
  Statement,
35
+ SubscriberInfo,
35
36
  User,
36
37
  )
37
38
  from ebicsclient.protocol import h005
@@ -240,13 +241,31 @@ class Client:
240
241
  ReturnCodeError: the bank reported a non-OK return code (e.g. no data available).
241
242
  CryptoError: the order data could not be decrypted.
242
243
  """
243
- if self._bank_keys is None:
244
- raise ClientStateError("Download requires the bank's keys; call hpb() first")
244
+ bank_keys = self._require_bank_keys("Download")
245
245
  logger.info("Download: opening a %s/%s transaction", btf.service_name, btf.message_name)
246
246
  request = h005.build_download_initialisation_request(
247
- self._bank, self._user, self._keyring, self._bank_keys, btf
247
+ self._bank, self._user, self._keyring, bank_keys, btf
248
+ )
249
+ return self._run_download_transaction(request, bank_keys)
250
+
251
+ def _require_bank_keys(self, operation: str) -> BankKeys:
252
+ if self._bank_keys is None:
253
+ raise ClientStateError(f"{operation} requires the bank's keys; call hpb() first")
254
+ return self._bank_keys
255
+
256
+ def _post_verified(self, request: bytes, bank_keys: BankKeys) -> bytes:
257
+ # Every ebicsResponse is authenticated with the bank's X002 signature; verify it
258
+ # BEFORE trusting anything in the response — including its return code.
259
+ response = self._transport.post(request)
260
+ h005.verify_response_signature(response, bank_keys.authentication)
261
+ return response
262
+
263
+ def _run_download_transaction(
264
+ self, initialisation_request: bytes, bank_keys: BankKeys
265
+ ) -> bytes:
266
+ initialisation = h005.parse_download_initialisation_response(
267
+ self._post_verified(initialisation_request, bank_keys)
248
268
  )
249
- initialisation = h005.parse_download_initialisation_response(self._transport.post(request))
250
269
 
251
270
  # Order data is one base64 stream split into NumSegments pieces: fetch the rest, in
252
271
  # order, driven by the authoritative segment count from the initialisation response.
@@ -259,7 +278,9 @@ class Client:
259
278
  number,
260
279
  last_segment=number == initialisation.num_segments,
261
280
  )
262
- segment = h005.parse_download_segment_response(self._transport.post(transfer))
281
+ segment = h005.parse_download_segment_response(
282
+ self._post_verified(transfer, bank_keys)
283
+ )
263
284
  segments.append(segment.order_data_segment)
264
285
 
265
286
  # Acknowledge the completed transfer so the bank marks the download as delivered.
@@ -267,7 +288,7 @@ class Client:
267
288
  receipt = h005.build_download_receipt_request(
268
289
  self._bank, self._keyring, initialisation.transaction_id
269
290
  )
270
- h005.parse_download_receipt_response(self._transport.post(receipt))
291
+ h005.parse_download_receipt_response(self._post_verified(receipt, bank_keys))
271
292
  logger.info(
272
293
  "Download: received %d segment(s) for transaction %s",
273
294
  initialisation.num_segments,
@@ -281,6 +302,60 @@ class Client:
281
302
  self._keyring.encryption, initialisation.transaction_key, encrypted_order_data
282
303
  )
283
304
 
305
+ def available_order_types(self) -> list[BusinessTransactionFormat]:
306
+ """Download the order types for which the bank currently holds data (HAA).
307
+
308
+ Runs the administrative ``HAA`` download and returns the Business Transaction
309
+ Formats with data waiting — an authoritative "is there anything to fetch?" check.
310
+ The bank's keys must already be available (call :meth:`hpb` first).
311
+
312
+ Returns:
313
+ The order types with data available (empty when nothing is waiting).
314
+
315
+ Raises:
316
+ ClientStateError: the bank's keys have not been fetched yet (run HPB first).
317
+ TransportError: a request could not be delivered.
318
+ ProtocolError: a response could not be parsed.
319
+ ResponseAuthenticationError: a response failed signature verification.
320
+ ReturnCodeError: the bank rejected the request.
321
+ CryptoError: the order data could not be decrypted.
322
+ """
323
+ bank_keys = self._require_bank_keys("HAA")
324
+ logger.info("HAA: requesting the order types with data available")
325
+ request = h005.build_admin_download_initialisation_request(
326
+ self._bank, self._user, self._keyring, bank_keys, "HAA"
327
+ )
328
+ return h005.parse_available_order_types(
329
+ self._run_download_transaction(request, bank_keys)
330
+ )
331
+
332
+ def subscriber_info(self) -> SubscriberInfo:
333
+ """Download the bank's registered data for this subscriber (HTD).
334
+
335
+ Runs the administrative ``HTD`` download and returns the bank's authoritative
336
+ view of the subscriber: the user's status and name, the permissions granted, and
337
+ every order type registered for the partner — the ground truth for diagnosing
338
+ rejected order types. The bank's keys must already be available (call :meth:`hpb`
339
+ first).
340
+
341
+ Returns:
342
+ The subscriber's registered data.
343
+
344
+ Raises:
345
+ ClientStateError: the bank's keys have not been fetched yet (run HPB first).
346
+ TransportError: a request could not be delivered.
347
+ ProtocolError: a response could not be parsed.
348
+ ResponseAuthenticationError: a response failed signature verification.
349
+ ReturnCodeError: the bank rejected the request.
350
+ CryptoError: the order data could not be decrypted.
351
+ """
352
+ bank_keys = self._require_bank_keys("HTD")
353
+ logger.info("HTD: requesting the subscriber data registered at the bank")
354
+ request = h005.build_admin_download_initialisation_request(
355
+ self._bank, self._user, self._keyring, bank_keys, "HTD"
356
+ )
357
+ return h005.parse_subscriber_info(self._run_download_transaction(request, bank_keys))
358
+
284
359
  def download_statements(self) -> list[Statement]:
285
360
  """Download the end-of-period camt.053 statements and parse them.
286
361
 
@@ -390,14 +465,15 @@ class Client:
390
465
  ReturnCodeError: the bank rejected the upload (e.g. a bad signature or order data).
391
466
  CryptoError: the order data could not be signed or encrypted.
392
467
  """
393
- if self._bank_keys is None:
394
- raise ClientStateError("Upload requires the bank's keys; call hpb() first")
468
+ bank_keys = self._require_bank_keys("Upload")
395
469
  logger.info("Upload: opening a %s/%s transaction", btf.service_name, btf.message_name)
396
- payload = h005.prepare_upload(self._user, self._keyring, self._bank_keys, order_data)
470
+ payload = h005.prepare_upload(self._user, self._keyring, bank_keys, order_data)
397
471
  request = h005.build_upload_initialisation_request(
398
- self._bank, self._user, self._keyring, self._bank_keys, btf, payload
472
+ self._bank, self._user, self._keyring, bank_keys, btf, payload
473
+ )
474
+ transaction_id = h005.parse_upload_initialisation_response(
475
+ self._post_verified(request, bank_keys)
399
476
  )
400
- transaction_id = h005.parse_upload_initialisation_response(self._transport.post(request))
401
477
 
402
478
  segments = payload.order_data_segments
403
479
  for number, segment_data in enumerate(segments, start=1):
@@ -409,7 +485,7 @@ class Client:
409
485
  segment_data,
410
486
  last_segment=number == len(segments),
411
487
  )
412
- h005.parse_upload_transfer_response(self._transport.post(transfer))
488
+ h005.parse_upload_transfer_response(self._post_verified(transfer, bank_keys))
413
489
  logger.info(
414
490
  "Upload: sent %d segment(s) for transaction %s", len(segments), transaction_id
415
491
  )
@@ -125,6 +125,15 @@ class ProtocolError(EbicsError):
125
125
  """The bank's response violated the expected EBICS protocol or could not be parsed."""
126
126
 
127
127
 
128
+ class ResponseAuthenticationError(ProtocolError):
129
+ """The bank's response failed authentication-signature verification.
130
+
131
+ Every ``ebicsResponse`` carries the bank's ``AuthSignature`` (X002) over its
132
+ ``authenticate="true"`` nodes. A missing or invalid signature means the response
133
+ cannot be attributed to the bank — do not trust its contents. Fail closed.
134
+ """
135
+
136
+
128
137
  class BankKeyMismatchError(ProtocolError):
129
138
  """The bank's HPB public keys did not match the pinned values.
130
139
 
@@ -450,6 +450,50 @@ class PaymentStatusReport:
450
450
  )
451
451
 
452
452
 
453
+ @dataclass(frozen=True, slots=True)
454
+ class OrderTypeInfo:
455
+ """One order type entry from the bank's subscriber data (HTD ``OrderInfo``/``Permission``).
456
+
457
+ Attributes:
458
+ admin_order_type: The administrative order type (``"BTD"``, ``"BTU"``, ``"HPB"``, …).
459
+ service: For ``BTD``/``BTU`` entries, the Business Transaction Format the entry
460
+ refers to; ``None`` for plain administrative order types.
461
+ description: The bank's human-readable description, or ``None`` (permissions
462
+ carry none).
463
+ number_of_signatures_required: How many electronic signatures the order type
464
+ requires (upload entries), or ``None`` if not stated.
465
+ """
466
+
467
+ admin_order_type: str
468
+ service: BusinessTransactionFormat | None
469
+ description: str | None
470
+ number_of_signatures_required: int | None
471
+
472
+
473
+ @dataclass(frozen=True, slots=True)
474
+ class SubscriberInfo:
475
+ """The bank's registered data for this subscriber (the HTD download).
476
+
477
+ The authoritative view of what the bank has configured: the order types offered to
478
+ the partner and the permissions granted to this user. Useful for diagnosing
479
+ ``091005``-style rejections — an order type missing here is not registered, whatever
480
+ a catalogue says.
481
+
482
+ Attributes:
483
+ user_id: The subscriber's User ID as registered at the bank.
484
+ user_status: The bank's user-status attribute (a bank-defined code), or ``None``.
485
+ name: The subscriber's registered name, or ``None``.
486
+ permissions: The order types this user may use, in document order.
487
+ order_types: Every order type registered for the partner, in document order.
488
+ """
489
+
490
+ user_id: str
491
+ user_status: str | None
492
+ name: str | None
493
+ permissions: tuple[OrderTypeInfo, ...]
494
+ order_types: tuple[OrderTypeInfo, ...]
495
+
496
+
453
497
  class OutputFormat(StrEnum):
454
498
  """The rendering format for the initialisation letter.
455
499